Spring Boot 工程开发常见问题解决方案,日常开发全覆盖

本文是 SpringBoot 开发的干货集中营,涵盖了日常开发中遇到的诸多问题,通篇着重讲解如何快速解决问题,部分重点问题会讲解原理,以及为什么要这样做。便于大家快速处理实践中经常遇到的小问题,既方便自己也方便他人,老鸟和新手皆适合,值得收藏 😄

1. 哪里可以搜索依赖包的 Maven 坐标和版本

  • https://mvnrepository.com/

    这个在2023年前使用得最多,但目前(2024)国内访问该网站时,经常卡死在人机校验这一步,导致无法使用

  • Maven Central

    刚开始我是临时用这个网站来替换前面那个,现在它越来越好用,就直接使用它了

2. 如何确定 SpringBoot 与 JDK 之间的版本关系

在 Spring官网 可以找到 SpringBoot 对应的 JDK 关系,但这种关系说明位于具体版本的参考手册(Reference Doc)中,按照以下图示顺序操作即可找到。

进入SpringBoot参考手册页面

点击 Quick Start

查看 System Requirement

重大版本与JDK及Spring基础框架的对应关系表

3. 如何统一处理Web请求的JSON日期格式问题

方式一:编程式声明

在 JacksonAutoConfiguration 装配前, 先装配一个 Jackson2ObjectMapperBuilderCustomizer,并在这个 Customizer 中设置日期格式。如下所示:

@Configuration
@ConditionalOnClass(ObjectMapper.class)
@AutoConfigureBefore(JacksonAutoConfiguration.class)     // 本装配提前于官方的自动装配
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
public class JacksonConfig {@Beanpublic Jackson2ObjectMapperBuilderCustomizer myJacksonCustomizer() {return builder -> {builder.locale(Locale.CHINA);builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");}
}    

方式二:配置式声明 <推荐>

参考下面的示例代码即可,关键之处是要指定 spring.http.converters.preferred-json-mapper 的值为 jackson, 否则配置不生效

spring:jackson:date-format: yyyy-MM-dd HH:mm:sslocale: zh_CNtime-zone: "GMT+8"http:converters:preferred-json-mapper: jackson ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄

4. 如何以静态方式访问Bean容器

写一个实现了 ApplicationContextAware 接口的类,通过该接口的 setApplicationContext()方法,获取 ApplicationContext, 然后用一个静态变量来持有它。之后便可以通过静态方法使用 ApplicationContext 了。Spring 框架在启动完成后,会遍历容器中所有实现了该接口的Bean,然后调用它们的setApplicationContext()方法,将ApplicationContext(也就是容器自身)作为参数传递过去。下面是示例代码:

import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;@Component
public class ApplicationContextHolder implements ApplicationContextAware {// 声明一个静态变量来持有 ApplicationContext private static ApplicationContext appContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {ApplicationContextHolder.appContext = applicationContext;}public static ApplicationContext getContext() {return ApplicationContextHolder.appContext;}}

5. 如何将工程打包成一个独立的可执行jar包

按以下三步操作即可(仅针对maven工程):

  • 在 pom.xml 中添加 spring boot 的构建插件

  • 为上一步的插件配置执行目标

  • 在工程目录下,命令行执行 maven clean package -Dmaven.test.skip=true

<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.1.6.RELEASE</version><executions><execution><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins>
</build>

🔔 关于 spring-boot-maven-plugin 插件的版本问题

如果不指定版本,默认会去下载最新的,这极有可能与代码工程所用的 jdk 版本不兼容,导致打包失败。那么应该用哪个版本呢?一个简单的办法,是先进入到本机的 Maven 仓库目录,然后再分别打开以下两个目录

  • org/springframework/boot/spring-boot

  • org/springframework/boot/spring-boot-maven-plugin

再结合自己工程的spring-boot版本(可通过IDE查看),选择相同版本或稍低版本的plugin插件

6. 如何从jar包外部读取配置文件

在 Java 启动命令中添加 spring-boot 配置文件相关参数,指定配置文件的位置,如下所示:

java -jar xxxx.jar --spring.config.location={yaml配置文件绝对路径}  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄

指定外部配置文件还有其它一些方式,详情参见 SpringBoot项目常见配置

📌 特别说明:

--spring.config.location 这个配置项一定要写在 xxxx.jar 之后,因为这是一个 SpringApplication 的参数,不是 java 命令的参数或选项,该参数最终是传递到了 main 方法的 args 变量上,因此在 main 方法中构建 SpringApplication 实例时,务必要把 args 参数传递过去,比如下面这两种写法

/** 样例A */
public static void main(String[] args) {SpringApplication.run(OverSpeedDataInsightMain.class);
}/** 样例B */
public static void main(String[] args) {SpringApplication.run(OverSpeedDataInsightMain.class, args); ̄ ̄ ̄
}

样例A由于没有传递args参数,因此通过命令行添加的 --spring.config.location 参数不会被SpringBoot实例读取到,在运行期间也就不会去读取它指定的配置文件了。

7. 如何同时启用多个数据源

方式一:手动创建多个My Batis的SqlSessionFactory

因为国内使用 MyBatis 框架最多,因此特别针对此框架单独说明。总体思路是这样的:

  • 多个数据源,各有各的配置

  • 针对每个数据源,单独创建一个 SqlSessionFactory

  • 每个 SqlSession 各自扫描不同数包和目录下的 Mapper.java 和 mapper.xml

  • 指定某个数据源为主数据源<强制>

样例工程部分代码如下,完整源码请访问码云上的工程 mybatis-multi-ds-demo

application.yml (点击查看)

spring:datasource:primary:driver: org.sqlite.JDBCurl: jdbc:sqlite::resource:biz1.sqlite3?date_string_format=yyyy-MM-dd HH:mm:ssminor:driver: org.sqlite.JDBCurl: jdbc:sqlite::resource:biz2.sqlite3?date_string_format=yyyy-MM-dd HH:mm:ss

主数据源装配 (点击查看)

@MapperScan(basePackages = {"cnblogs.guzb.biz1"},sqlSessionFactoryRef = "PrimarySqlSessionFactory"
)
@Configuration
public class PrimarySqlSessionFactoryConfig {// 表示这个数据源是默认数据源,多数据源情况下,必须指定一个主数据源@Primary@Bean(name = "PrimaryDataSource")@ConfigurationProperties(prefix = "spring.datasource.primary")public DataSource getPrimaryDateSource() {// 这个MyBatis内置的无池化的数据源,仅仅用于演示,实际工程请更换成具体的数据源对象return new UnpooledDataSource();}@Primary@Bean(name = "PrimarySqlSessionFactory")public SqlSessionFactory primarySqlSessionFactory(@Qualifier("PrimaryDataSource") DataSource datasource) throws Exception {SqlSessionFactoryBean bean = new SqlSessionFactoryBean();bean.setDataSource(datasource);// 主数据源的XML SQL配置资源Resource[] xmlMapperResources = new PathMatchingResourcePatternResolver().getResources("classpath:mappers/primary/*.xml");bean.setMapperLocations(xmlMapperResources);return bean.getObject();}@Primary@Bean("PrimarySqlSessionTemplate")public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("PrimarySqlSessionFactory") SqlSessionFactory sessionFactory) {return new SqlSessionTemplate(sessionFactory);}
}

副数据源装配 (点击查看)

@Configuration
@MapperScan(basePackages = {"cnblogs.guzb.biz2"},sqlSessionFactoryRef = "MinorSqlSessionFactory"
)
public class MinorSqlSessionFactoryConfig {@Bean(name = "MinorDataSource")@ConfigurationProperties(prefix = "spring.datasource.minor")public DataSource getPrimaryDateSource() {// 这个MyBatis内置的无池化的数据源,仅仅用于演示,实际工程请更换成具体的数据源对象return new UnpooledDataSource();}@Bean(name = "MinorSqlSessionFactory")public SqlSessionFactory primarySqlSessionFactory(@Qualifier("MinorDataSource") DataSource datasource) throws Exception {SqlSessionFactoryBean bean = new SqlSessionFactoryBean();bean.setDataSource(datasource);// 主数据源的XML SQL配置资源Resource[] xmlMapperResources = new PathMatchingResourcePatternResolver().getResources("classpath:mappers/minor/*.xml");bean.setMapperLocations(xmlMapperResources);return bean.getObject();}@Bean("MinorSqlSessionTemplate")public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("MinorSqlSessionFactory") SqlSessionFactory sessionFactory) {return new SqlSessionTemplate(sessionFactory);}
}

方式二:使用路由式委托数据源 AbstractRoutingDataSource <推荐>

上面这种方式,粒度比较粗,在创建SqlSessionFactory时,将一组Mapper与DataSource绑定。如果想粒度更细一些,比如在一个Mapper内,A方法使用数据源A, B方法使用数据源B,则无法做到。

Spring 官方有个 AbstractRoutingDataSource 抽象类, 它提供了以代码方式设置当前要使用的数据源的能力。其实就是把自己作为 DataSource 的一个实现类,并将自己作为数据源的集散地(代理人),在内部维护了一个数据源的池,将 getConnection() 方法委托给这个池中对应的数据源。

DynamicDataSource.java

public class DynamicDataSource extends AbstractRoutingDataSource {/** 通过 ThreadLocal 来记录当前线程中的数据源名称 */private final ThreadLocal<String> localDataSourceName = new ThreadLocal<>();public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {super.setDefaultTargetDataSource(defaultTargetDataSource);super.setTargetDataSources(targetDataSources);}@Overrideprotected Object determineCurrentLookupKey() {return localDataSourceName.get();}public void setDataSourceName(String dataSourceName) {localDataSourceName.set(dataSourceName);}public void clearDataSourceName() {localDataSourceName.remove();}
}

DynamicDataSourceConfig

@Configuration
public class DynamicDataSourceConfig {// 表示这个数据源是默认数据源,多数据源情况下,必须指定一个主数据源@Primary@Bean(name = "dynamic-data-source")@DependsOn(DataSourceName.FIRST)public DynamicDataSource getPrimaryDateSource(@Qualifier(DataSourceName.FIRST) DataSource defaultDataSource,@Qualifier(DataSourceName.SECOND) @Autowired(required = false) DataSource secondDataSource) {System.out.println("first=" + defaultDataSource + ", second = " + secondDataSource);Map<Object, Object> allTargetDataSources = new HashMap<>();allTargetDataSources.put(DataSourceName.FIRST, defaultDataSource);allTargetDataSources.put(DataSourceName.SECOND, secondDataSource);return new DynamicDataSource(defaultDataSource, allTargetDataSources);}@Bean(name= DataSourceName.FIRST)@ConfigurationProperties(prefix = "spring.datasource.first")public DataSource createFirstDataSource() {// 这个MyBatis内置的无池化的数据源,仅仅用于演示,实际工程请更换成具体的数据源对象return new UnpooledDataSource();}@Bean(name= DataSourceName.SECOND)@ConfigurationProperties(prefix = "spring.datasource.second")public DataSource createSecondDataSource() {// 这个MyBatis内置的无池化的数据源,仅仅用于演示,实际工程请更换成具体的数据源对象return new UnpooledDataSource();}}

SwitchDataSourceTo

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SwitchDataSourceTo {/** 数据源的名称 */String value() default DataSourceName.FIRST;}

SwitchDataSourceAspect

@Aspect
@Component
public class SwitchDataSourceAspect {@AutowiredDynamicDataSource dynamicDataSource;@Around("@annotation(switchDataSourceTo)")public Object around(ProceedingJoinPoint point, SwitchDataSourceTo switchDataSourceTo) throws Throwable {String dataSourceName = switchDataSourceTo.value();try {dynamicDataSource.setDataSourceName(dataSourceName);System.out.println("切换到数据源: " + dataSourceName);return point.proceed();} finally {System.out.println("执行结束,准备切换回到主数据源");dynamicDataSource.setDataSourceName(DataSourceName.FIRST);}}
}

Biz1Mapper

@Mapper
public interface Biz1Mapper {// 未指定数据源,即为「默认数据源」@Select("select * from user")List<UserEntity> listAll();@SwitchDataSourceTo(DataSourceName.FIRST)@Select("select * from user where id=#{id}")UserEntity getById(@Param("id") Long id);
}

Biz2Mapper

@Mapper
public interface Biz2Mapper {@Select("select * from authority")@SwitchDataSourceTo(DataSourceName.SECOND)List<AuthorityEntity> listAll();// 本方法没有添加 SwitchDataSourceTo 注解,因此会使用默认的数据源,即 first// 但 first 数据源中没有这个表。该方法会通过在程序中手动设置数据源名称的方式,来切换@Select("select count(*) as quantity from authority")Integer totalCount();}

完整源码请访问码云上的工程 mybatis-multi-ds-demo

方式三:使用 MyBatisPlus 的 多数据源方案 <推荐>

MyBatisPlus 增加了对多数据源的支持,详细做法请参考 MyBatis多数据源官方手册,它的底层原理与方式二一致,但特性更多,功能出更完善。若有兴趣的话,建议将这个多数据源的功能单独做成一个 jar 包或 maven 依赖。以使其可以在非 MyBatis 环境中使用。

多数据源切换引起的事务问题

对于纯查询类非事务性方法,上面的多数据源切换工作良好,一旦一个Service方法开启了事务,且内部调用了多个有不同数据源的Dao层方法,则这些数据源切换均会失败。原因为切换数据源发生在openConnection()方法执行时刻,但一个事务内只有一个Connection。当开启事务后,再次切换数据源时,由于已经有connection了,此时切换会无效。

因此解决办法为:先切换数据源,再开启事务。开启事务后,不能再切换数据源了。

8. 如何同时启用多个Redis连接

最简单的办法是直接使用 Redis官方的客户端库,但这样脱离了本小节的主旨。业务代码中使用spring 的 redis 封装,主要是使用 RedisTemplate 类,RedisTemplate 封装了常用的业务操作,但它并不关注如何获得 redis 的连接。这个工作是交由 RedisConnectionFactory 负责的。因此,RedisTemplate 需要指定一个 RedisConnectionFactory。由此可知,在工程中,创建两个RedisConnectionFactory, 每个连接工厂连接到不同的 redis 服务器即可。以下简易示例代码中,两个连接工厂连接的是同一个服务器的不同数据库。

创建两个 RedisConnectionFactory 和两个 RedisTemplate

@Configuration
public class RedisConfiguration {/** * 0号数据库的连接工厂* 本示例没有使用早期的 JedisConnectionFactory, 而是选择了并发性更好的 LettuceConnectionFactory, 下同*/@Primary@Bean("redis-connection-factory-db0")   // 明确地指定 Bean 名称,该实例将作为依赖项,传递给相应的 RedisTemplate, 下同 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄public RedisConnectionFactory createLettuceConnectionFactory0() {// 这里使用的是单实例Redis服务器的连接配置类,// 哨兵与集群模式的服务器,使用对应的配置类设置属性即可。// 另外,这里没有演示通过yaml外部配置文件来设置相应的连接参数,因为这不是本小节的重点RedisStandaloneConfiguration clientProps = new RedisStandaloneConfiguration();clientProps.setHostName("localhost");clientProps.setPort(6379);clientProps.setDatabase(0);return new LettuceConnectionFactory(clientProps);}/** 1号数据库的连接工厂 */@Bean("redis-connection-factory-db1") ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄public RedisConnectionFactory createLettuceConnectionFactory1() {RedisStandaloneConfiguration clientProps = new RedisStandaloneConfiguration();clientProps.setHostName("localhost");clientProps.setPort(6379);clientProps.setDatabase(1);return new LettuceConnectionFactory(clientProps);}/** * 操作0号数据库的 RedisTemplate, * 创建时,直接将0号数据库的 RedisConnectionFactory 实例传递给它*/@Primary@Bean("redis-template-db-0")public RedisTemplate<String, String> createRedisTemplate0(@Qualifier("redis-connection-factory-db0") RedisConnectionFactory factory0) { ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(factory0);redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new StringRedisSerializer());return redisTemplate;}/** * 操作1号数据库的 RedisTemplate, * 创建时,直接将1号数据库的 RedisConnectionFactory 实例传递给它*/@Bean("redis-template-db-1")public RedisTemplate<String, String> createRedisTemplate1(@Qualifier("redis-connection-factory-db1") RedisConnectionFactory factory1) { ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(factory1);redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new StringRedisSerializer());return redisTemplate;}}

多Redis连接的测试验证代码

@Component
@SpringBootApplication
public class MultiRedisAppMain {// 注入操作0号数据库的Redis模板@Resource(name = "redis-template-db-0")RedisTemplate redisTemplate0;// 注入操作1号数据库的Redis模板@Resource(name = "redis-template-db-1")RedisTemplate redisTemplate1;public static void main(String[] args) {SpringApplication.run(MultiRedisAppMain.class, args);}@EventListener(ApplicationReadyEvent.class)public void operateBook() {redisTemplate0.opsForValue().set("bookName", "三体");redisTemplate0.opsForValue().set("bookPrice", "102");redisTemplate1.opsForValue().set("bookName", "老人与海");redisTemplate1.opsForValue().set("bookPrice", "95");}
}

本小节完整的示例代码已上传到 multi-redis-demo

9. 如何同时消费多个 Kafka Topic

9.1 同时消费同一 Kakfa 服务器的多个topic

这个是最常见的情况,同时也是最容易实现的,具体操作是:为 @KafkaListener 指定多个 topic 即可,如下所示

点击查看代码

/** 多个topic在一个方法中消费的情况 */
@KafkaListener(topics = {"topic-1", "topic-2", "topic-3"}, groupId = "group-1") ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
public void consumeTopc1_2_3(String message) {System.out.println("收到消息 kafka :" + message);
}/** 不同 topic 在不同方法中消费的情况 */
@KafkaListener(topics = "topic-A", groupId = "group-1")
public void consumeTopicA(String message) {System.out.println("收到消息 kafka :" + message);
}/** 不同 topic 在不同方法中消费的情况 */
@KafkaListener(topics = "topic-B", groupId = "group-1")
public void consumeTopicB(String message) {System.out.println("收到消息 kafka :" + message);
}

9.2 同时消费不同Kafka服务器的多个topic

这种情况是本小节的重点,与 spring 对 redis 的封装不同,spring 对 kafka 官方的 client lib 封装比较重,引入了以下概念

  • ConsumerFactroy

    消费者工厂,该接口能创建一个消费者,它将创建与消息系统的网络连接

  • MessageListenerContainer

    消息监听器容器,这是 spring 在 Consumer 之上单独封装出来的概念,顾名思义,该组件的作用是根据监听参数,创建一个消息监听器。看上去它似乎与 Consumer 组件要干的事一样,但在 spring 的封装结构里,consumer 实际上只负责连接到消息系统,然后抓取消息,抓取后如何消费,是其它组件的事,MessageLisntener 便是这样的组件,而 MessageListenerContainer 是创建 MessageListener 的容器类组件。

  • KafkaListenerContainerFactory

    消息监听器容器的工厂类,即这个组件是用来创建 MessageListenerContainer 的,而 MessageListenerContainer 又是用来创建 MessageLisntener 的。

看了上面3个重要的组件的介绍,你一定会产生个疑问:创建一个监听器,需要这么复杂吗?感觉一堆的工厂类,这些工厂类还是三层套娃式的。答案是:如果仅仅针对 Kafka,不需要这么复杂。spring 的这种封装是要建立一套『事件编程模型』来消费消息。并且还是跨消息中间件的,也就是说,无论是消费 kafka 还是 rabbitmq , 它们的上层接口都是这种结构。为了应对不同消息系统间的差异,才引出了这么多的工厂类。

但不得不说,作为一个具体的使用者而言,这就相当于到菜单市买一斤五花肉,非得强行塞给你二两边角料,实得五花肉只有8两不说,那二两完全是多余的,既浪费又增加负担。spring 官方的这种封装,让它们的程序员爽了,但使用者的负担却是增加了。我们愿意花大把时间来学习 Spring Framework 和 Spring Boot 的编程思想和源代码,因为这两个是非常基础的通用框架。但是对具体产品的过渡封装,使用者大多是不喜欢的,因为我们可没那么多时间来学习它的复杂设计。毕竟这些只是工具的封装,不是一个可部署的产品。业务代码要基于它们来实现功能,谁也不想错误堆栈里全是一堆第三访库的类,而不是我们自己写的代码。尽管spring 的工具质量很好。但复杂的包装增加了使用难度,概念没有理解到位、某个理解不透彻的参数配置不对、某个完全没听说过的默认配置项在自己特定的环境下出错,这些因素导致的异常,都会让开发者花费巨大的时间成本来解决。因此,对于有复杂需求的同仁们,建议大家还是直接使用 kafka 官方提供的原生 client lib, 自己进行封装,这样可以做到完全可控。

回到主题,要实现同时连接多个不同的kafka服务器,提供相应服务器的 ConsumerFactory 即可。只是 ConsumerFactory 实例还需要传递给 KafkaListenerContainerFactory,最后在 @KafkaLisntener 注解中指定要使用的 KafkaListenerContainerFactory 名称即可。

连接多个 Kafka 服务器的组件配置类

@Configuration
public class KafkaConfiguration {@Primary@Bean("consumerFactory") ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄public ConsumerFactory createConsumerFactory() {Map<String, Object> consumerProperties = new HashMap<>();consumerProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");consumerProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());consumerProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());return new DefaultKafkaConsumerFactory<>(consumerProperties);}// 第二个消费工厂,为便于实操, 这里依然连接的是同一个 Kafka 服务器@Bean("consumerFactory2") ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄public ConsumerFactory createConsumerFactory2() {Map<String, Object> consumerProperties = new HashMap<>();consumerProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");consumerProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());consumerProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());return new DefaultKafkaConsumerFactory<>(consumerProperties);}@Primary// 自己创建的监听容器工厂实例中,一定要有一个实例的名字叫: kafkaListenerContainerFactory,// 因为 KafkaAnnotationDrivenConfiguration 中也默认配置了一个 KafkaListenerContainerFactory,// 这个默认的 KafkaListenerContainerFactory 名称就叫 kafkaListenerContainerFactory,// 其装配条件就是当容器中没有名称为 kafkaListenerContainerFactory 的Bean时,那个装配就生效,// 如果不阻止这个默认的KafkaListenerContainerFactory装备,会导致容器中有两个 KafkaListenerContainerFactory,这会引入一些初始化问题@Bean("kafkaListenerContainerFactory")public KafkaListenerContainerFactory<KafkaMessageListenerContainer> createContainerFactory1(ConcurrentKafkaListenerContainerFactoryConfigurer configurer,@Qualifier("consumerFactory") ConsumerFactory consumerFactory) { ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ConcurrentKafkaListenerContainerFactory listenerContainerFactory = new ConcurrentKafkaListenerContainerFactory();configurer.configure(listenerContainerFactory, consumerFactory);return listenerContainerFactory;}// 第二个监听器容器工厂@Bean("kafkaListenerContainerFactory2")public KafkaListenerContainerFactory<KafkaMessageListenerContainer> createContainerFactory2(ConcurrentKafkaListenerContainerFactoryConfigurer configurer,@Qualifier("consumerFactory2") ConsumerFactory consumerFactory2) { ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ConcurrentKafkaListenerContainerFactory listenerContainerFactory = new ConcurrentKafkaListenerContainerFactory();configurer.configure(listenerContainerFactory, consumerFactory2);return listenerContainerFactory;}
}

连接多 Kafka 服务器的测试主程序

@Component
@EnableKafka
@SpringBootApplication
public class MultiKafkaAppMain {public static void main(String[] args) {SpringApplication.run(MultiKafkaAppMain.class, args);}@KafkaListener(topics = "topic1", groupId = "g1", containerFactory = "kafkaListenerContainerFactory") ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄public void consumeKafka1(String message) {System.out.println("[KAFKA-1]: 收到消息:" + message);}@KafkaListener(topics = "topic-2", groupId = "g1", containerFactory = "kafkaListenerContainerFactory2") ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄public void consumeKafka2(String message) {System.out.println("[KAFKA-2]: 收到消息:" + message);}@EventListener(ApplicationReadyEvent.class)public void init() {System.out.println("[MAIN]: 启动成功,等待Kakfa消息");}
}

本小节完整的示例代码已上传到 multi-kafka-demo

10. 如何查看程序启动后所有的 Properties

方式一:遍历Environment对象

Spring Boot 中有个 Environment 接口,它记录了当前激活的 profile 和所有的「属性源」,下面是一段在 runtime 期间打印所有 properties 的示例代码

PrintAllPropetiesDemo.java(点击查看)

@Component
public class PrintAllPropetiesDemo {@ResourceEnvironment env;@EventListener(ApplicationReadyEvent.class)public void printAllProperties throws Exception {// 打印当前激活的 profileSystem.out.println("Active profiles: " + Arrays.toString(env.getActiveProfiles()));// 从「环境」对象中,获取「属性源」final MutablePropertySources sources = ((AbstractEnvironment) env).getPropertySources();// 打印所有的属性,包括:去重、脱敏StreamSupport.stream(sources.spliterator(), false).filter(ps -> ps instanceof EnumerablePropertySource).map(ps -> ((EnumerablePropertySource) ps).getPropertyNames()).flatMap(Arrays::stream)// 去除重复的属性名.distinct()// 过滤敏感属性内容.filter(prop -> !(prop.contains("credentials") || prop.contains("password"))).forEach(prop -> System.out.println(prop + ": " + env.getProperty(prop)));}
}

方式二:查看 Spring Acuator 的 /env 监控页面 <推荐>

先引入 acuator 的依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

然后在配置 acuator 的 web 访问 uri

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {return http.authorizeExchange().pathMatchers("/actuator/**").permitAll().anyExchange().authenticated().and().build();
}

假定端口为8080, 则访问 http://localhost:8080/acuator/env 便能看到工程运行起来后所有的 properties 了

11. 如何申明和使用异步方法

在 SpringBoot 中使用异步方法非常简单,只要做以下同步

  • 启用异步特性

  • 在要异步执行的方法中,添加 @Async 注解

下面是一段示例代码

// 启用异步特性
@EnableAsync
public class BookService {@Async   // 声明要异步执行的方法public void disableAllExpiredBooks(){....}
}

📣 特别说明

以上代码确实可以让 disableAllExpiredBook() 方法异步执行,但它的执行方式是: 每次调用此方法时,都新创建一个线程,然后在新线程中执行这个方法。如果方法调用得不是很频繁,这个做法是OK的。但如果方法调用得很频繁,就会导致系统频繁地开线程,而创建线程的开销是比较大的。Spring 已经考虑到了这个场景,只需要为异步执行的方法指定一个执行器就可以了,而这个执行器通常都是一个具备线程池功能的执行器。示例代码如下:

@EnableAsync
public class BookService {@Async("bookExcutor")  // 在注解中指定执行器 ̄ ̄ ̄ ̄ ̄ ̄ ̄public void disableAllExpiredBooks(){....}
}@Configuration
public class ExecutorConfiguration {// 装配书籍任务的通用执行器@Bean("bookExcutor") ̄ ̄ ̄ ̄ ̄ ̄ ̄public Executor speedingArbitrationExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(6);executor.setMaxPoolSize(24);executor.setQueueCapacity(20000;executor.setKeepAliveSeconds(30);executor.setThreadNamePrefix("书籍后台任务线程-");executor.setWaitForTasksToCompleteOnShutdown(true);// 任务队列排满后,直接在主线程(提交任务的线程)执行任务,异步执行变同步executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());return executor;}
}

12. 如何快速添加 boot 的 maven 依赖项

Spring Boot 是一个以Boot为中心的生态圈,当我们指定了boot的版本后,如果要使用中生态圈中的组件,就不用再指定该组件的版本了。有两种方式可达到此目的。

  • 方式一:项目工程直接继承 Boot Starter Parent POM

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.1.5</version>
</parent>

  • 方式二:在pom.xml的依赖管理节点下,添加 spring-boot-dependencies

<dependencies><!-- ② 这里添加starter依赖,但不用指定版本 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency>
</dependencies......<dependencyManagement><dependencies><!-- ① 在这里添加spring-boot的依赖pom --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.7.16</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>

同理,如果要引入 Spring Cloud 生态圈中的相关组件,也建议通过「方式二」,把 spring-cloud-dependencies 加入到依赖管理节点下

13. 如何以静态方式获取 HttpServletRequest 和 HttpServletResponse

通过 spring-web 组件提供的 RequestContextHolder 中的静态方法来获取 HttpServletRequest 和 HttpServletResponse,如下所示:

import org.springframework.web.util.WebUtils;import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;public class WebTool extends WebUtils {public static HttpServletRequest getHttpRequest() {RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;return servletRequestAttributes.getRequest();}public static HttpServletResponse getHttpResponse() {RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;return servletRequestAttributes.getResponse();}
}

14. 如何解决 ConfigurationProperties 不生效的问题

如果你在自己的 Properties 类上添加了 @ConfigurationProperties 注解,启动程序后没有效果,可参考下面这两种方法来解决:

  • 方式一1. 在启动类添加 @EnableConfigurationProperties 注解

    2. 在 @ConfigurationProperties 标注的类上添加 @Component 注解 (@Service注解也可以)

    启动类

@SpringBootApplication
@EnableAutoConfiguration
@EnableConfigurationProperties 
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
public class MyBootApp {public static void main(String[] args) {SpringApplication.run(MyBootApp.clss, args);}
}

自定义的 Properties 类

@Component
 ̄ ̄ ̄ ̄ ̄ ̄ ̄
@ConfigurationProperties(prefix="gzub.hdfs")
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
public class HdfsProperties {private String nameNode;private String user;private String password;
}

方式二1. 在启动类添加 @ConfigurationPropertiesScan 注解,并指定要扫描的 package2. 在自定义的 Properties 类上添加 @ConfigurationProperties(不需要添加 @Component 注解)

启动类

@SpringBootApplication
@ConfigurationPropertiesScan({"vip.guzb"})
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
public class MyBootApp {public static void main(String[] args) {SpringApplication.run(MyBootApp.clss, args);}
}

自定义的 Properties 类

@ConfigurationProperties(prefix="gzub.hdfs")
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
public class HdfsProperties {private String nameNode;private String user;private String password;
}

15. 如何统一处理异常

  1. 编写一个普通的Bean,不继承和实现任何类与接口

  2. 在该Bean的类级别上添加 @RestControllerAdvice 注解,向框架声明这是一个可跨 Controller 处理异常、初始绑定和视图模型特性的类

  3. 在类中编写处理异常的方法,并在方法上添加 @ExceptionHandler 注解,向框架声明这是一个异常处理方法

    编写异常处理方法的要求如下:

    方法是 public 的

    方法必须用 @ExceptionHandler 注解修饰

    方法的返回值就是最终返给前端的内容,通常是JSON文本

    方法参数中,需指定要处理的异常类型

  4. 如果需要对特定异常做特殊的处理,则重复第3步

下面是一较完整的示例代码(点击查看)

import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.http.ResponseEntity
import org.springframework.http.HttpStatus;@RestControllerAdvice
public class MyGlobalExceptionHandlerResolver {/** 处理最外层的异常 */@ExceptionHandler(Exception.class)public ResponseEntity<ErrorResponse> handleException(Exception e) {List details = new ArrayList();details.add(e.getMesssage());ErrorResponse error = new ErrorResponse(e.getMessage, details);return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);}/** 处理业务异常,这里使用了另外一种方式来设置 http 响应码 */@ExceptionHandler(BusinessException.class)@ResponseStatus(HttpStatus.HTTP_BAD_REQUEST)public ErrorResponse handleException(BusinessException e) {List details = new ArrayList();details.add(e.getBackendMesssage());return new ErrorResponse(e.getFrontendMessage(), details);}
}/** 返回给前端的错误内容对象 */
public class ErrorResponse {private String message;private List<String> details;......
}/** 业务异常 */
public class BusinessException extends RuntimeException{private String frontendMessage;private String backendMessage;......
}

16. 应该对哪些异常做特殊处理

对于Web开发而言,我们应该在全局异常处理类中,对以下异常做特殊处理

  • Exception

  • BusinessExecption

  • HttpRequestMethodNotSupportedException

  • HttpClientErrorException

  • FeignException

  • ConstraintViolationException

  • ValidationException

17. 异常处理组件应该具备的特性

  1. 业务异常处理

    异常信息中,要明确区分出前端展示内容与后端错误内容

    后端错误内容可再进一步分为「错误的一般描述信息」和「详细的错误列表」

    前后端错误信息中,应过滤敏感内容,如身份证、密码等,且过滤机制提供开关功能,以方便开发调试

    异常信息中,应该包含业务流水号,便于调试和排查线上问题时,将各个节点的错误内容串联起来

    多数情况下,业务异常都不应该打印堆栈,只需要在日志中输出第一个触发业务异常的代码位置即可

    因为业务异常是我们在编码阶段就手动捕获了的,也就是说,这些异常是可预期的,并且是我们自己手动编码抛出的。因此,只需要输出该异常的抛出点代码位置,异常堆栈是没有意义的,它只会增加日志的存储体积

    另外,多数业务异常都是在检查业务的执行条件时触发的,比如:商品不存在、库存不足、越权访问、输入数据不合规等。且这类错误会频繁发生,若输出其堆栈的话,日志中会大量充斥着这样的异常堆栈。它既增加了日志的存储体积,也干扰了正常日志内容的查看

    异常信息中,要详细记录错误内容,尽可能把异常现场的信息都输出。这是开发人员最容易给自己和他人挖坑的地方,比如:一个业务异常的日志输出内容是这样:“积分等级不够”。这个异常信息是严重不足的,它缺少以下这些重要信息,以致极难在线上排查问题:

    谁的积分等级不足

    这个用户当前的积分是多少

    他要拥有多少积分,和什么样的等级

    他在访问什么资源

    注意:您可能会有疑问,把用户账号输出到日志就可以了,没必要输出它当前的积分,因为积分可以去数据库查。但这样做是不行的,因为:

    生产环境的数据库研发人员是不能直接访问的,让运维人员查,效率不高还增加运维工作量

    数据查询出来的值,也不是发生异常当时的值,时光荏然,你大妈已经不你大妈了 😁

    即使是个相对静态(变动不频繁)的参数,运行期代码所使用的值,也极有可能与数据库中不一致。比如程序启动时,没有从数据库中加载,而是使用了默认值,又或者是某个处理逻辑将它的值临时改变了

  2. 非业务异常

    尽可能地捕获所有异常

    一定要在日志中输出非业务异常的堆栈<重要>

    尽量不要二次包装非业务异常,如果一定要包装,「务必」在将包装后的异常 throw 前,先输出原始异常的堆栈信息

18. 为什么出错了却没有异常日志

在 WebMVC 程序中,通常都有一全局异常处理器(如15小节所述),因此,有异常一定是会被捕获,并输出日志的。不过,这个全局异常处理器,仅对Web请求有效,如果是以下以下情况,则需要在代码中手动捕获和输出异常日志:

  • 在非WEB请求的线程中运行的代码比如定时任务中的代码所产生的异常。如果没有捕获和输出异常日志,那么发生了异常也不知道,只能从结果数据上判断,可能发生了错误,但却无法快速定位。

  • 从Web请求线程中脱离出来的异步线程中的代码这种情况更常见,同时也要非常小心。比如异步发送短信,异步发邮件等,一定要做好异常处理

19. 如何处理异常日志只有一行简短的文本

比如下面这个经典的场景

java.lang.NullPointerException

异常信息只有这么一行,没有代码位置,没有causedException, 更没有堆栈。这是因为JVM有个快速抛出(FastThrow)的异常优化:如果相同的异常在短时间内集中大量throw,则将这些异常都合并为同一个异常对象,且没有堆栈。

解决办法为:java 启动命令中,添加-OmitStackTraceInFastThrow这个JVM选项,如:

java -XX:-OmitStackTraceInFastThrow -jar xxxx.jar

📌 说明1

JVM只对以下异常做FastThrow优化

  • NullPointerException

  • ArithmeticException

  • ArrayStoreException

  • ClassCastException

  • ArrayIndexOutOfBoundsException

📌 说明2

出现此问题,基本上意味着代码有重大缺陷,跟死循环差不多,不然不会出现大量相同的常集中抛出。另外,开启该选项后,若这种场景出现,是会刷爆日志存储的。当然,相比之下找到问题更重要,该选项是否要在生产环境开启,就自行决定吧。

20. 如何解决同一实例内部方法调用时,部分事务失效的问题

事务失效示例代码(点击查看)

@Service
public class BookService {@ResourceBookDao bookDao;public void changePrice(Long bookId, Double newPrice) {doChangePrice(bookId, newPrice);logOperation();sendMail();}@Transactional(rollbackFor = Exception.class)public void resetPrice(Long bookId, Double newPrice) {doChangePrice(bookId, newPrice);logOperation();sendMail();}@Transactional(rollbackFor = Exception.class)public void doChangePrice(Long bookId, Double newPrice) {bookDao.setPrice(bookId, newPrice);}@Transactional(rollbackFor = Exception.class)public void logOperation(Long bookId, Double newPrice) {.... // 省略记录操作日志的代码}public void sendMail(Long bookId, Double newPrice) {.... // 省略发送邮件的代码}
}

上述代码,调用 changePrice() 方法时,如果 sendMail() 方法在执行时发生了异常,则前面的 doChangePrice() 和 logOperation() 所执行的数据库操作均不会回滚。但同样的情形如果发生在 resetPrice() 方法上,doChangePrice() 和 logOperation() 均会回滚。

这个例子还可以进行更细化的演进,不过核心原因都是一个:Spring 对注解事务的实现手段,是通过 CGLib 工具库创建一个继承这个业务类的新类,捕获原业务类方法执行期间的异常,然后执行回滚的。但是对原业务类中,方法内部对其它方法的调用,这个被调用的方法,其上的事务注解则不再生效。如果直接在外部调用这些方法,则事务注解是生效的。

以上面的示例代码为准, changePrice() 方法内部分别调用了 doChangePrice()、logOperation()、sendMail() 三个方法,但由于 changePrice() 方法本身并没有添加事务注解,因此,它内部调用的 doChangePrice()、logOperation() 这两个方法的事务注解是不生效的。因此,实际上执行过程都没有开启事务。当然,如果是从外部直接单独调用 doChangePrice() 和 logOperation(),则二者的事务均生效。

解决办法:在外部单独调用这些有事务注解的方法。如果需要将这些方法组合在一个方法体内,整体完成一个业务逻辑,也在其它类中创建方法,在该方法中调用这些有事务注解的方法完成逻辑组织。

21. 如何阻止某个第三方组件的自动装配

方法一:配置 @SpringBootApplication 注解的 exclude 属性

如下代码所示:

// 启动时,将Spring官方的数据源自动装配排除
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})       
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
public class MyAppMain{public static void main(String[] args) {SpringApplication.run(MyAppMain.class, args);}
}

方法二:在配置文件中指定 <推荐>

方法一需要修改代码,对于普通的业务系统而言,是能不改代码就坚决不改。因此推荐下面这种配置的方式来指定:

spring:autoconfigure:# 指定要排除的自动装配类,多个类使用英文逗号分隔exclude: org.springframework.cloud.gateway.config.GatewayAutoConfiguration

方法三:临时注释掉该组件的 @EnableXXX 注解比如常见的 @EnableConfigurationProperies 、@EnalbeAsync 、@EnableJms 等,在代码中临时注释掉这些注解即可。但仅适用于提供了这种 Enable 注解方式装配的组件。

未完待续~~~

文章转载自:顾志兵

原文链接:https://www.cnblogs.com/guzb/p/spring-boot-common-development-issue-solution-list.html

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

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

相关文章

护眼台灯对眼睛有危害吗?多款预防近视的台灯推荐

在日常生活中&#xff0c;灯光对于我们而言&#xff0c;是非常重要的&#xff0c;尤其是在夜晚&#xff0c;不管是学习还是办公都需要合适的光线环境。很多家长为了保护孩子的视力会选择从台灯下手&#xff0c;但又不知道护眼台灯对眼睛有危害吗&#xff1f;今天就来好好的告诉…

【MySQL】数据库--表操作

目录 一、创建表 二、查看表 三、修改表 1. 添加字段--add 2.修改表名--rename to 3.修改列名--change 4.修改字段的数据类型--modify 5.删除字段&#xff08;列&#xff09;--drop 四、删除表 一、创建表 create [temporary]table[if not exists]table_name [([colu…

python如何获取word文档的总页数

最近在搞AI. 遇到了一个问题&#xff0c;就是要进行doc文档的解析。并且需要展示每个文档的总页数。 利用AI. 分别尝试了chatGPT, 文心一言&#xff0c; github copilot&#xff0c;Kimi 等工具&#xff0c;给出来的答案都不尽如人意。 给的最多的查询方式就是下面这种。 这个…

【Canvas与艺术】硬朗风格十二棱表表盘

【效果图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>硬朗风格十二棱表表盘</title><style type"text/css…

Qt+OpenGL入门教程(三)——绘制三角形

通过前两篇文章的学习&#xff0c;我想大家应该有了基本的理解&#xff0c;我们接下来实操一下。 创建Qt OpenGL窗口 QOpenGLWidget QGLWidget是传统QtOpenGL模块的一部分&#xff0c;与其他QGL类一样&#xff0c;应该在新的应用程序中避免使用。相反&#xff0c;从Qt5.4开始…

新手如何用Postman做接口自动化测试?

1、什么是自动化测试 把人对软件的测试行为转化为由机器执行测试行为的一种实践。 例如GUI自动化测试&#xff0c;模拟人去操作软件界面&#xff0c;把人从简单重复的劳动中解放出来&#xff0c;本质是用代码去测试另一段代码&#xff0c;属于一种软件开发工作&#xff0c;已…

【检索增强】Retrieval-Augmented Generation for Large Language Models:A Survey

本文简介 1、对最先进水平RAG进行了全面和系统的回顾&#xff0c;通过包括朴素RAG、高级RAG和模块化RAG在内的范式描述了它的演变。这篇综述的背景下&#xff0c;更广泛的范围内的法学硕士研究RAG的景观。 2、确定并讨论了RAG过程中不可或缺的核心技术&#xff0c;特别关注“…

深入Facebook的世界:探索数字化社交的无限可能性

引言 随着数字化时代的到来&#xff0c;社交媒体平台已经成为了人们日常生活中不可或缺的一部分&#xff0c;而其中最为突出的代表之一便是Facebook。作为全球最大的社交媒体平台之一&#xff0c;Facebook不仅仅是一个社交网络&#xff0c;更是一个数字化社交的生态系统&#…

机器人机械手加装SycoTec 4060 ER-S电主轴高精密铣削加工

随着科技的不断发展&#xff0c;机器人技术正逐渐渗透到各个领域&#xff0c;展现出前所未有的潜力和应用价值。作为机器人技术的核心组成部分之一&#xff0c;机器人机械手以其高精度、高效率和高稳定性的优势&#xff0c;在机械加工、装配、检测等领域中发挥着举足轻重的作用…

Python学习:lambda(匿名函数)、装饰器、数据结构

Python Lambda匿名函数 Lambda函数&#xff08;或称为匿名函数&#xff09;是Python中的一种特殊函数&#xff0c;它可以用一行代码来创建简单的函数。Lambda函数通常用于需要一个函数作为输入的函数&#xff08;比如map()&#xff0c;filter()&#xff0c;sort()等&#xff0…

C++ 多线程和互斥锁(一文搞定)

实验 简介&#xff1a;我们启动并行启动两个线程&#xff0c;但设置一个全局互斥锁&#xff0c;在两个线程中等待并占用互斥锁&#xff0c;然后输出日志。 代码 #include <iostream> #include <thread> /* C 多线程库 */ #include <mutex> …

基于Hive的天气情况大数据分析系统(通过hive进行大数据分析将分析的数据通过sqoop导入到mysql,通过Django基于mysql的数据做可视化)

基于Hive的天气情况大数据分析系统&#xff08;通过hive进行大数据分析将分析的数据通过sqoop导入到mysql&#xff0c;通过Django基于mysql的数据做可视化&#xff09; Hive介绍&#xff1a; Hive是建立在Hadoop之上的数据仓库基础架构&#xff0c;它提供了类似于SQL的语言&…

2015年认证杯SPSSPRO杯数学建模A题(第二阶段)绳结全过程文档及程序

2015年认证杯SPSSPRO杯数学建模 A题 绳结 原题再现&#xff1a; 给绳索打结是人们在日常生活中常用的技能。对登山、航海、垂钓、野外生存等专门用途&#xff0c;结绳更是必不可少的技能之一。针对不同用途&#xff0c;有多种绳结的编制方法。最简单的绳结&#xff0c;有时称…

机器学习和深度学习的简单对比

如图1-2所示&#xff0c;深度学习&#xff08;DeepLearning&#xff0c;DL&#xff09;属于机器学习的子类。它的灵感来源于人类大脑的工作方式&#xff0c;这是利用深度神经网络来解决特征表达的一种学习过程。深度神经网络本身并非是一个全新的概念&#xff0c;可理解为包含多…

实战 | 微调训练TrOCR识别弯曲文本

导 读 本文主要介绍如何通过微调训练TrOCR实现弯曲文本识别。 背景介绍 TrOCR&#xff08;基于 Transformer 的光学字符识别&#xff09;模型是性能最佳的 OCR 模型之一。在我们之前的文章中&#xff0c;我们分析了它们在单行打印和手写文本上的表现。 TrOCR—基于Transforme…

系统分析师-数学与经济管理

系统架构设计师 系统架构设计师-软件开发模型总结 文章目录 系统架构设计师前言一、最小生成树二、最短路径三、网络与最大流量四、不确定型决策 前言 数学是一种严谨、缜密的科学&#xff0c;学习应用数学知识&#xff0c;可以培养系统架构设计师的抽象思维能力和逻辑推理能…

【Python】python+requests+excel+unittest+ddt实现接口自动化实例

目录 测试需求实现思路框架代码实例1. 环境准备和配置文件2. Excel接口数据及测试结果3. API封装4. 读取Excel数据5. 测试用例6. 日志和配置文件处理7. HTMLTestRunner生成可视化的html报告8. 报告通过飞书/邮件发送报告通过飞书发送报告通过邮件发送9. 入口函数10. 飞书Webhoo…

Git 命令总览

Git Git 是一个版本控制系统&#xff0c;用于管理项目代码。通过 Git 可以轻松地进行代码的提交、更新和合并&#xff0c;确保项目代码的安全性和稳定性。同时&#xff0c;Git 还提供了丰富的工具和功能&#xff0c;如分支管理、代码审查、版本回退等&#xff0c;帮助开发更好…

五种免费的Python开发环境及具体下载网址

五种免费的Python开发环境及具体下载网址 目录 五种免费的Python开发环境及具体下载网址1.Anaconda2.PyCharm Community Edition3.Visual Studio Code4.Jupyter Notebook5. WinPython Python编程可选择不同的开发工具环境进行&#xff0c;本文介绍五种常用的&#xff0c;读者可…

vue前端工程化

前言 本文介绍的是有关于vue方面的前端工程化实践&#xff0c;主要通过实践操作让开发人员更好的理解整个前端工程化的流程。 本文通过开发准备阶段、开发阶段和开发完成三个阶段开介绍vue前端工程化的整体过程。 准备阶段 准备阶段我将其分为&#xff1a;框架选择、规范制…