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 {
    @Bean
    public 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:ss
    locale: zh_CN
    time-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;
 
    @Override
	public 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.JDBC
      url: jdbc:sqlite::resource:biz1.sqlite3?date_string_format=yyyy-MM-dd HH:mm:ss
    minor:
      driver: org.sqlite.JDBC
      url: 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);
    }
 
    @Override
    protected 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 {
 
    @Autowired
    DynamicDataSource 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 {
    @Resource
    Environment env;
 
    @EventListener(ApplicationReadyEvent.class)
    public void printAllProperties throws Exception {
        // 打印当前激活的 profile
        System.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 {
    @Resource
    BookDao 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快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

移动端开发思考:Uniapp的上位替代选择

文章目录 前言跨平台开发技术需求技术选型uniappFlutterMAUIAvalonia安卓原生 Flutter开发尝试Avalonia开发测试测试项目新建项目代码MainViewMainViewModel 发布/存档 MAUI实战&#xff0c;简单略过打包和Avalonia差不多 总结 前言 作为C# .NET程序员&#xff0c;我有一些移动…

Python图像处理——计算机视觉中常用的图像预处理

概述 在计算机视觉项目中&#xff0c;使用样本时经常会遇到图像样本不统一的问题&#xff0c;比如图像质量&#xff0c;并非所有的图像都具有相同的质量水平。在开始训练模型或运行算法之前&#xff0c;通常需要对图像进行预处理&#xff0c;以确保获得最佳的结果。图像预处理…

MySQL---触发器

一、介绍 触发器是与表有关的数据库对象&#xff0c;指在insert/update/delete之前(BEFORE)或之后(AFTER)&#xff0c;触发并执行触发器中定义的SQL语句集合。触发器的这种特性可以协助应用在数据库端确保数据的完整性, 日志记录 , 数据校验等操作 。 使用别名OLD和NEW来引用触…

Ubuntu20.04安装OpenCV并在vsCode中配置

1. 安装OpenCV 1.1 安装准备&#xff1a; 1.1.1 安装cmake sudo apt-get install cmake 1.1.2 依赖环境 sudo apt-get install build-essential libgtk2.0-dev libavcodec-dev libavformat-dev libjpeg-dev libswscale-dev libtiff5-dev sudo apt-get install libgtk2.0-d…

go的通信Channel

go的通道channel是用于协程之间数据通信的一种方式 一、channel的结构 go源码&#xff1a;GitHub - golang/go: The Go programming language src/runtime/chan.go type hchan struct {qcount uint // total data in the queue 队列中当前元素计数&#xff0c;…

Day54:WEB攻防-XSS跨站Cookie盗取表单劫持网络钓鱼溯源分析项目平台框架

目录 XSS跨站-攻击利用-凭据盗取 XSS跨站-攻击利用-数据提交 XSS跨站-攻击利用-flash钓鱼 XSS跨站-攻击利用-溯源综合 知识点&#xff1a; 1、XSS跨站-攻击利用-凭据盗取 2、XSS跨站-攻击利用-数据提交 3、XSS跨站-攻击利用-网络钓鱼 4、XSS跨站-攻击利用-溯源综合 漏洞原理…

智慧管道物联网远程监控解决方案

智慧管道物联网远程监控解决方案 智慧管道物联网远程监控解决方案是近年来在智能化城市建设和工业4.0背景下&#xff0c;针对各类管道网络进行高效、安全、精准管理的前沿科技应用。它融合了物联网技术、大数据分析、云计算以及人工智能等多种先进技术手段&#xff0c;实现对管…

玫瑰图和雷达图(自备)

目录 玫瑰图 数据格式 绘图基础 绘图升级&#xff08;文本调整&#xff09; 玫瑰图 下载数据data/2020/2020-11-24 mirrors_rfordatascience/tidytuesday - 码云 - 开源中国 (gitee.com) R语言绘图—南丁格尔玫瑰图 - 知乎 (zhihu.com) 数据格式 rm(list ls()) libr…

Unity | 工具类-UV滚动

一、内置渲染管线Shader Shader"Custom/ImageRoll" {Properties {_MainTex ("Main Tex", 2D) "white" {}_Width ("Width", float) 0.5_Distance ("Distance", float) 0}SubShader {Tags {"Queue""Trans…

AugmentedReality之路-显示隐藏AR坐标原点(3)

本文介绍如何显示/隐藏坐标原点&#xff0c;分析AR坐标原点跟手机的位置关系 1、AR坐标原点在哪里 当我们通过AugmentedReality的StartARSession函数打开AR相机的那一刻&#xff0c;相机所在的位置就是坐标原点。 2、创建指示箭头资产 1.在Content/Arrow目录创建1个Actor类…

腾讯云4核8G服务器多少钱?12M带宽646元15个月,买1年送3月

2024年腾讯云4核8G服务器租用优惠价格&#xff1a;轻量应用服务器4核8G12M带宽646元15个月&#xff0c;CVM云服务器S5实例优惠价格1437.24元买一年送3个月&#xff0c;腾讯云4核8G服务器活动页面 txybk.com/go/txy 活动链接打开如下图&#xff1a; 腾讯云4核8G服务器优惠价格 轻…

记录minio、okhttp、kotlin一连环的版本冲突问题

问题背景 项目中需要引入minio&#xff0c;添加了如下依赖 <dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.2</version></dependency> 结果运行报错&#xff1a; Caused by: java.la…

黑群晖基于docker配置frp内网穿透

前言 我的黑群晖需要设置一下内网穿透来外地访问&#xff0c;虽然zerotier的p2p组网已经很不错了&#xff0c;但是这个毕竟有一定的局限性&#xff0c;比如我是ios的国区id就下载不了zerotier的app&#xff0c;组网不了 1.下载镜像 选择第一个镜像 2.映射文件 配置frpc.ini&a…

基于Spring Boot 3 + Spring Security6 + JWT + Redis实现登录、token身份认证

基于Spring Boot3实现Spring Security6 JWT Redis实现登录、token身份认证。 用户从数据库中获取。使用RESTFul风格的APi进行登录。使用JWT生成token。使用Redis进行登录过期判断。所有的工具类和数据结构在源码中都有。 系列文章指路&#x1f449; 系列文章-基于Vue3创建前端…

【机器学习300问】55、介绍推荐系统中的矩阵分解算法是什么、有什么用、怎么用?

本来这篇文章我想先讲矩阵分解算法是什么东西的&#xff0c;但这样会陷入枯燥的定义中去&#xff0c;让原本非常有趣技术在业务场景中直观的使用路径被切断。所以我觉得先通过一个具体的推荐算法的例子&#xff0c;来为大家感性的介绍矩阵分解有什么用会更加合理。 如果你还不知…

iOS开发进阶(十一):ViewController 控制器详解

文章目录 一、前言二、UIViewController三、UINavigationController四、UITabBarController五、UIPageViewController六、拓展阅读 一、前言 iOS 界面开发最重要的首属ViewController和View&#xff0c;ViewController是View的控制器&#xff0c;也就是一般的页面&#xff0c;…

WordPress Git主题 响应式CMS主题模板

分享的是新版本&#xff0c;旧版本少了很多功能&#xff0c;尤其在新版支持自动更新后&#xff0c;该主题可以用来搭建个人博客&#xff0c;素材下载网站&#xff0c;图片站等 主题特点 兼容 IE9、谷歌 Chrome 、火狐 Firefox 等主流浏览器 扁平化的设计加响应式布局&#x…

MySQL count(*/column)查询优化

count()是SQL中一个常用的聚合函数&#xff0c;其被用来统计记录的总数&#xff0c;下面通过几个示例来说明此类查询的注意事项及应用技巧。 文章目录 一、count()的含义二、count()的应用技巧2.1 同时统计多列2.2 利用执行计划 一、count()的含义 count()用于统计符合条件的记…

常用的8个应用和中间件的Docker运行示例

文章目录 1、Docker Web 管理工具 portainer2、在线代码编辑器 Code Server3、MySQL4、Redis5、Nginx6、PostgreSQL7、媒体管理工具 Dim8、Gitlab 1、Docker Web 管理工具 portainer Portainer 是一个轻量级的管理 UI &#xff0c;可让你轻松管理不同的 Docker 环境&#xff0…

API是什么,如何保障API安全

随着移动APP、微服务架构、云、物联网的兴起&#xff0c;企业API数量呈爆发式增长。作为数字世界的连接者&#xff0c;API为企业搭建起了一条内外相连、四通八达的“数据公路”。 API是什么&#xff1f;API&#xff0c;全称Application Programming Interface&#xff0c;即应用…