Spring Boot 工程开发常见问题解决方案,日常开发全覆盖
本文是 SpringBoot 开发的干货集中营,涵盖了日常开发中遇到的诸多问题,通篇着重讲解如何快速解决问题,部分重点问题会讲解原理,以及为什么要这样做。便于大家快速处理实践中经常遇到的小问题,既方便自己也方便他人,老鸟和新手皆适合,值得收藏 😄
1. 哪里可以搜索依赖包的 Maven 坐标和版本
-
这个在2023年前使用得最多,但目前(2024)国内访问该网站时,经常卡死在人机校验这一步,导致无法使用
-
刚开始我是临时用这个网站来替换前面那个,现在它越来越好用,就直接使用它了
2. 如何确定 SpringBoot 与 JDK 之间的版本关系
在 Spring官网 可以找到 SpringBoot 对应的 JDK 关系,但这种关系说明位于具体版本的参考手册(Reference Doc)中,按照以下图示顺序操作即可找到。
重大版本与JDK及Spring基础框架的对应关系表
Spring Boot 版本 | JDK 版本 | Spring Framework 版本 |
---|---|---|
2.7.18 | JDK8 + | 5.3.31 + |
3.2.3 | JDK17 + | 6.1.4 + |
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 注解,并指定要扫描的 package
2. 在自定义的 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. 如何统一处理异常
-
编写一个普通的Bean,不继承和实现任何类与接口
-
在该Bean的类级别上添加 @RestControllerAdvice 注解,向框架声明这是一个可跨 Controller 处理异常、初始绑定和视图模型特性的类
-
在类中编写处理异常的方法,并在方法上添加 @ExceptionHandler 注解,向框架声明这是一个异常处理方法
编写异常处理方法的要求如下:
- 方法是 public 的
- 方法必须用 @ExceptionHandler 注解修饰
- 方法的返回值就是最终返给前端的内容,通常是JSON文本
- 方法参数中,需指定要处理的异常类型
-
如果需要对特定异常做特殊的处理,则重复第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. 异常处理组件应该具备的特性
-
业务异常处理
-
异常信息中,要明确区分出前端展示内容与后端错误内容
-
后端错误内容可再进一步分为「错误的一般描述信息」和「详细的错误列表」
-
前后端错误信息中,应过滤敏感内容,如身份证、密码等,且过滤机制提供开关功能,以方便开发调试
-
异常信息中,应该包含业务流水号,便于调试和排查线上问题时,将各个节点的错误内容串联起来
-
多数情况下,业务异常都不应该打印堆栈,只需要在日志中输出第一个触发业务异常的代码位置即可
- 因为业务异常是我们在编码阶段就手动捕获了的,也就是说,这些异常是可预期的,并且是我们自己手动编码抛出的。因此,只需要输出该异常的抛出点代码位置,异常堆栈是没有意义的,它只会增加日志的存储体积
- 另外,多数业务异常都是在检查业务的执行条件时触发的,比如:商品不存在、库存不足、越权访问、输入数据不合规等。且这类错误会频繁发生,若输出其堆栈的话,日志中会大量充斥着这样的异常堆栈。它既增加了日志的存储体积,也干扰了正常日志内容的查看
-
异常信息中,要详细记录错误内容,尽可能把异常现场的信息都输出。
这是开发人员最容易给自己和他人挖坑的地方,比如:一个业务异常的日志输出内容是这样:“积分等级不够”。这个异常信息是严重不足的,它缺少以下这些重要信息,以致极难在线上排查问题:- 谁的积分等级不足
- 这个用户当前的积分是多少
- 他要拥有多少积分,和什么样的等级
- 他在访问什么资源
注意:您可能会有疑问,把用户账号输出到日志就可以了,没必要输出它当前的积分,因为积分可以去数据库查。但这样做是不行的,因为:
- 生产环境的数据库研发人员是不能直接访问的,让运维人员查,效率不高还增加运维工作量
- 数据查询出来的值,也不是发生异常当时的值,时光荏然,你大妈已经不你大妈了 😁
- 即使是个相对静态(变动不频繁)的参数,运行期代码所使用的值,也极有可能与数据库中不一致。比如程序启动时,没有从数据库中加载,而是使用了默认值,又或者是某个处理逻辑将它的值临时改变了
-
-
非业务异常
- 尽可能地捕获所有异常
- 一定要在日志中输出非业务异常的堆栈<重要>
- 尽量不要二次包装非业务异常,如果一定要包装,「务必」在将包装后的异常 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(),则二者的事务均生效。
方案一:手动隔离
可以通过在外部(其它Class)单独调用这些有事务注解的方法来使事务生效,如果需要将这些方法组合在一个方法体内,整体完成一个业务逻辑,也在其它类中创建方法,在该方法中调用这些有事务注解的方法完成逻辑组织。这是开发者最能理解的方式,代码也较直观。
方案二:明确调用代理对象的方法 <推荐>
事务失效的原因是同一个类中,无事务的方法,调用了相同实例的有事务注解的方法所致。如果在编码阶段,就能在这个无事务的方法内明确声明:运行期调用的是由Aop代理的实例的方法,则此问题可解。
Spring 提供了 AopContext 类来获取被切面代理的实例,利用它便能够在方法中,明确调用本Class的代理实例方法了。操作如下:
-
先在代码中明确调用AOP代理的Class
以上面的 BookService 为例子,变更后的内容如下:
点击查看代码
@Service public class BookService { @Resource BookDao bookDao; public void changePrice(Long bookId, Double newPrice) { // ① 直接拿到 BookService 的AOP代理实例 BookService proxyOfThis= (BookService) AopContext.currentProxy(); proxyOfThis.doChangePrice(bookId, newPrice); // ② proxyOfThis.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) { .... // 省略发送邮件的代码 } } 由于直接获得了 BookService 的 AOP 代理实例,因此上述代码中 ② 和 ③ 处的调用,其事务都会生效。
-
启用 Spring Aop 的代理对象暴露特性
默认情况情况下,上述代码 ① 处获得的实例依然是原始对象。需要开启「AOP代理对象暴露」特性,① 处的代码才会生效。具体操作,就是在启动类上加上 @EnableAspectJAutoProxy(exposeProxy = true) 注解,比如:
@SpringBootApplication @EnableAspectJAutoProxy(exposeProxy = true) public class MainApp { public static void main(String[] args) { .... } }
📌 特别说明
开启「AOP代理对象暴露」特性会影响 Spring 容器的性能,所以框架默认未开启此特性
本方法仅对AOP代理对象有效,非AOP代理对象是无效的,比如:@Sync 注解的方法,其所在的实例,虽然也是代理对象,但不是基于切面的代理对象,因此,该方法对此类代理对象是无效的。
幸运的是 @Transactional 注解会创建一个切面,故而在可用此方法来解决本小节提及的问题
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 注解方式装配的组件。
22. 如何进行Body、Query、Path Variable类型的参数校验
-
Http Body 实体类型的参数校验
这里特指 JSON 格式的 Body 体,这是最常见的情况。步骤如下:
1. 编写一个类用来接收JSON格式的Body参数。这个类的要求如下
- 字段需有相应的 Getter 和 Setter 方法
- 在要做校验的字段上添加相应的约束注解,如 @NotBlank
2. 对应的 Controller 方法参数中,使用 @RequestBody 修饰类型为第1步中写的类
3. 对应的 Controller 方法参数中,使用 @Valid 或 @Validated 修饰类型为第1步中写的类
示例代码(点击查看)
/** 接收 http body JSON 参数的对象 */ public class AddRoleRequest { @NotBlank(message = "角色编码不能为空") private String code; @NotBlank(message = "角色名称不能为空") private String name; @NotEmpty(message = "角色适用的区域等级不能为空") private List<RegionLevel> regionLevels; // Getter & Setter } @RestController @RequestMapping(path="/role") public class RoleController { // 这里的 @Valid 也可以换成 @Validated @PostMapping(path="/add") public void addRole(@Valid @RequestBody AddRoleRequest addRequest) { // 业务代码 ... } } -
Query 与 Path Variable 类型的参数校验
如下所示,下划线部分就是 Query 参数, 而{}中的内容就是 Path Vairable
http://demo.guzb.vip/books/{china}/list-roles?code=system-admin®ionLevel=COUNTY  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ 默认情况下,Query 与 Path Variable 参数验证是没有效果的。可通过以下步骤开启该类型的参数校验:
- 提供一个 MethodValidationPostProcessor 来处理 @Validated 注解。
- 在 Controller 类上添加 @Validated 注解
⚠️ 注意:将 @Validate 注解添加在 Controller 的方法上或方法的参数上,均不会使 MethodValidationPostProcessor 生效,也就不能执行Query与PathVariable参数的校验
示例代码(点击查看)
@Configuration public class ValidationConfig { /** * 重点是要添加一个MethodValidationPostProcessor实到到Bean容器中, * URL路径参数(Path Vairable)和查询参数(Query Parameter)的校验才会生效 */ @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); } } @Validated // 这个注解必须加在 Controller 类级别上,MethodValidationPostProcessor 才会生效  ̄ ̄ ̄ ̄ ̄ ̄ @RestController @RequestMapping(path="/books") public class RoleController { @PostMapping(path="/{region}") public void addRole( @PathVairable("region") region, @RequestParam("author") @Size(min=2, message="作者名称长度不能小于2")author) { // 业务代码 ... } } Query 与 Path Variable 参数验证是不需要在Controller方法的参数签名上加 @Validated 修饰的
Spring Validation 框架在参数检验未通过时,会抛出 ConstraintViolationException 异常,因此应该在全局异常处理类中(请参考第15小节),添加对它的处理。
ConstraintViolationException 异常处理示例代码(点击查看)
@ExceptionHandler(ConstraintViolationException.class) public final ResponseEntity<ErrorResponse> handleConstraintViolation(ConstraintViolationException ex) { List<String> details = ex.getConstraintViolations() .stream() .map(ConstraintViolation::getMessage) .collect(Collectors.toList()); ErrorResponse error = new ErrorResponse(BAD_REQUEST, details); return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); }
📣 特别说明
23. 如何级联校验多层级的参数对象
如果一个对象,它的拥有的字段,都是String 和 Java 的原始(Primitive)类型或其对应的包装类型,那么这些字段上具体的JSR-303校验注解是会生效的。但如果字段类型是数组、集合、或自定义的其它类,则我们在自定类的义字段上添加的JSR-303校验注解,默认情况下是不生效的。
有两种方式使上述多层级复杂结构对象上的校验注解生效:在复杂类型字段上添加 @Valid 注解,或在复杂类型的 class 定义上添加 @Valid注解,如下所示:
public class BookVo { @NotBlank(message = "图书名称不能为空") private String name; @Valid // 在author字段上添加该注解后,其内部的JSR-303约束注解就能生效了 private AuthorVo author; // 出版商字段没有添加 @Valid 注解,而是在 PublisherVo 类的定义上添加了该注解 privaet PublisherVo publisher; } public class AuthorVo { @NotBlank(message = "作者名称不能为空") private String name; @NotNull(message = "年龄不为空") @Range(min=6, max=120, message = "年龄必须在 6~120 以内"); private Integer age; } /** 注解直接作用在类上,这样所有类型为该类的字段,校验都会生效 */ @Valid public class Publisher { @NotBlank(message = "出版商名称不能为空") private name; @NotBlank(message = "出让商地址不能为空") private String address; }
24. 如何在程序启动完毕后自动执行某个任务
-
方式一:实现 InitializingBean接口
InitializingBean 是 Spring 基础框架提供的一个工 Bean 生命周期接口,它只有一个名为 afterPropertiesSet() 的方法。如果一个受容器管理的类实现了 InitializingBean,那么 Spring 容器在初始化完这个类后,会调用它的 afterPropertiesSet() 方法。比如下面这段示例代码:
import org.springframework.stereotype.Component; import org.springframework.beans.factory.InitializingBean; @Component public class AuthenticationFailureLimitor implements InitializingBean { private LimitSettings limitSettings; // 这是一个模拟的业务方法:限制认证失败的次数 public void tryBlock(int failureCount) { if (limitSettings.isEnable && failureCount > limitSettings.getMaxFailureCount) { doBlock(); } } /** * 本方法将在初化完成后(即所有需要自动注入的字段都被赋值后)调用 * 这里在方法中模拟对限制设置对象的初始加载 */ @Override public void afterPropertiesSet() throws Exception { this.limitSettings = loadLmitSettings(); } } -
方式二:使用 @PostContruct 注解修饰要在启动完成后立即执行的方法
@PostContruct 是Java JSR-250 基础规范中的标准注解,
import javax.annotation.PostConstruct; import org.springframework.stereotype.Component; @Component public class AuthenticationFailureLimitor { // 其它业务代码 ...... @PostConstruct public void init() { System.out.println("init()方法将在启动完成后执行"); } // 方法的作用域即使是 private 也可以 @PostConstruct private void setup() { System.out.println("setup()方法将在启动完成后执行"); } } 上述示例代码中的 init() 和 setup() 方法都会在启动完成后执行
-
方式三:监听容器事件 <推荐>
Spring 基础框架提供了事件机制,在容器启动的各个阶段中,均会向容器内的组件广播相应的事件,以便业务代码或第三方组件添加自己的扩展逻辑。有两种方法来监听事件。
1. 实现 ApplicationListener 接口
这是在 Spring4.2 版本以前的标准做法,接口定义了 onApplicationEvent () 方法,接口声明支持范型,可以指定要监听事件类型,这里需要监听的事件为 ApplicationReadyEvent。示例代码如下:
import org.springframework.stereotype.Component; import org.springframework.context.ApplicationListener; @Component public class OneceTask implements ApplicationListener<ApplicationReadyEvent> { // 指定要监听的事件  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ @Override public void onApplicationEvent(ApplicationReadyEvent event) { System.out.println("onApplicationEvent()方法将在容器启动完成后执行"); } }
2. 用 @EventLisnter 注解修饰目标方法 <推荐>
随着注解方式装配容器成为主流,Spring4.2 版本以后引入了 @EventListener 来快速实现事件监听。示例代码如下:
import org.springframework.stereotype.Component; import org.springframework.context.event.EventListener; @Component public class OnceTask { // 监听的事件还可以换成 ApplicationReadyEvent, 作用域也可以是 private 的 @EventListener(ApplicationStartedEvent.class) private void doSomething(ApplicationStartedEvent event){ System.out.println("我是容器启动后的初始化任务"); } } -
方式四:实现 CommandLineRunner 接口 <推荐>
SpringBoot 第一个版本就有这个接口,当应用容器完成所有Bean的装配后(对应的事件为 ApplicationStarted,此时应用还不能接收外部请求),将调用该接口内的方法。示例代码如下:
import org.springframework.stereotype.Component; import org.springframework.boot.CommandLineRunner; @Component public class OnceTask implements CommandLineRunner { // 监听的事件还可以换成 ApplicationReadyEvent @Override public void run(String... args) throws Exception { System.out.println("我将在应用容器完成所有Bean的装配后执行"); } }
📣 特别说明
严格说来,事件监听方式和 CommandLineRunner 方式才是最符合要求的,它才是真正意义上的「容器启动完成后」执行的方法。另外两种方式实际上是在「Bean 初始化完成后」执行的。
假定有一个类,集中使用了上面的所有方式,那么这些方法谁先执行谁后执行呢 ?看看下面这个例子:
示例代码(点击查看)
@Comonent public class OnceTask implements InitializingBean, ApplicationListener, CommandLineRunner { @PostConstruct private void setup() { System.out.println("OnceTask.@PostConstruct"); } @EventListener private void onStartedByAnnotation(ApplicationStartedEvent event){ System.out.println("OnceTask.@EventListener(ApplicationStartedEvent)"); } @EventListener private void onReadyByAnnotation(ApplicationReadyEvent event){ System.out.println("OnceTask.@EventListener(ApplicationReadyEvent)"); } @Override public void afterPropertiesSet() throws Exception { System.out.println("OnceTask .afterPropertiesSet()"); } @Override public void onApplicationEvent(ApplicationStartedEvent event) { System.out.println("OnceTask.onStartedByInterface(ApplicationStartedEvent)"); } @Override public void run(String... args) throws Exception { System.out.println("OnceTask.commandLineRunner"); } }
以上代码的执行结果为:
OnceTask.@PostConstruct OnceTask.afterPropertiesSet() OnceTask.onStartedByInterface(ApplicationStartedEvent) OnceTask.@EventListener(ApplicationStartedEvent) OnceTask.commandLineRunner OnceTask.@EventListener(ApplicationReadyEvent)
另外,使用XML装配方式时,在XML的<Bean>标签中,还可以通过 init-method 属性指定某个类的初始化方法,其作用与 @PostContrust 注解和 InitializingBean 的 afterProperties() 方法一致。但由于目前(2023年)国内所有新项目都使用注解来装配容器,这里就不再详细介绍它了。
25. 如何解决Bean装配过程中的循环依赖
循环依赖示意图:
┌───┐ ┌───┐ ┌───┐ │ A │ --- Depends On --> │ B │ --- Depends On --> │ C │ └───┘ └───┘ └─┬─┘ ↑ │ └─────────────────── Depends On ──────────────────┘
Spring 本身是不支持循环依赖的,在程序启动期间,Bean 容器会检查是否存在循环依赖,如果存在,则直接启动失败,同时也会在日志中输出循环依赖的 Bean 信息。
最好的办法是在设计上避免循环依赖,如果实在避免不了,可以通过「手动装配部分」依赖的方式来解决。即让 Spring 完成无循环依赖的部分,在程序启动完毕后,再手动完成涉及循环依赖部分。下面是一个示例(示例的代码注释阐述了实现原理):
存在循环依赖问题的原始代码(点击查看)
//这个设计中,SerivceA 和 ServiceB 相互依赖对方,导致容器启动失败 @Service public class ServiceA { @Autowired private ServiceB serviceB; } @Service public class ServiceB { @Autowired private ServiceA serviceA; }
通过手动装配部分依赖,解决循环依赖问题(点击查看)
@Service public class ServiceA { // serviceB 字段的值不交由 Spring 容器处理,由我们手动赋值 private ServiceB serviceB; /** * 这里通过「应用程序启动完成」事件,通过Bean容器中取出 ServiceB 实例 * 然后再手动赋值给 serviceB 字段,解决循环依赖问题 * * 说明一:ApplicationStartedEvent 事件表明所有 Bean 已装配完成,但此时尚未发生任何对外服务的调用, * 而整个系统可以对外提供服务的事件是 ApplicationReadyEvent, 因此这里使用 Started 事件更合适 * * 说明二:手动装配的关键是「拿到ApplicationContext」和「在合适的时机进行手动装配」,这个合适的时机就是 ApplicationStartedEvent * 「拿到ApplilcationContext对象」还有其它一些办法,参见「第13小节」 * 「合适的时机」同样也有一些其它的选择,参见「第24小节」 */ @EventListener(ApplicationStartedEvent.class) void setCycleDependencyFields(ApplicationStartedEvent appStartedEvent) { ServiceB serviceB = appStartedEvent.getApplicationContext().getBean(ServiceB.class); this.serviceB = serviceB; } } // ServiceB 正常装配 @Service public class ServiceB { @Autowired private ServiceA serviceA; }
26. 如何在所有Web请求的前后执行自己的代码
采用以下两种方式中的一种即可
-
Sevlet 过滤器
-
SpringMvc的HandlerInterceptor接口
27. 如何统一给配置项属性值加密
一般说来,研发人员是接触不到生产环境中的配置文件的,正规的项目,也不会将生产环境的信息内置到源代码Jar包中。因此,多数 C 端的项目是不需要对配置文件进行加密的。有此要求的大多是 toB 或 toG 类项目。
统一给配置文件中的指定属性值加密,可以使用 Jasypt 来完成。Jasypt 原本只是一个加密解密的基础工具,但经过进一步封装增强后的 jasypt-spring-boot-starter,便能够统一地对 SpringBoot 项目中的加密配置属性进行解密。全程是自动的,默认情况下,只需要进行以下两步设置:
-
设置 jasypt 的加密密钥
Jasypt 默认的加密类的是 SimpleAsymmetricStringEncryptor, 它的密钥取自属性 jasypt.encryptor.password
-
生成密文并标识其需要由 Jasypt 做解密处理
将要加密的明文,提前用第1步的密钥生成密文,然后将其包裹在ENC()的括号中,这样 Jasypt 就会在属性加载完毕后,对被 ENC() 包裹的属性值进行解密,并用解密后的明文替换原来的值。
下面是实操步骤:
引入依赖
<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <!-- SpringBoot3 以上的版本,使用3.x的版本,反之使用2.x的版本 --> <version>2.1.2</version> </dependency>
配置文件中针敏感属性加密
# 无需加密的配置项,以明文配置 demo-app: username: westing-loafer password: continuous-wandering # 加密后的配置项,PropertySource 加载后,[Jasypt][jasypt] 会查找 被 ENC() 包裹的配置属性项,然后将其解码 database: username: ENC(QTQhWWDOt4c2u3gHzd50F38nkQriShqE) # ① password: ENC(NMiiJQbnMQFhZBbmiEa+LEe7Ps+u+DmWNd1JATXXPWs=) # ② # Jasypt 的加密密钥,解密配置项时,也使用该密钥。这里将其注释,因为正式的项目,不会将密钥写在配置文件中 # 详见 com.ulisesbocchio.jasyptspringboot.encryptor.DefaultLazyEncryptor#createPBEDefault() 方法 # jasypt.encryptor.password: cnblogs
在主方法中验证
点击查看代码
@SpringBootApplication public class JaspytDemoAppMain implements CommandLineRunner { @Autowired Environment env; public static void main(String[] args) { args = checkOrSetDefaultJasyptPassword(args); // ③ SpringApplication.run(JaspytDemoAppMain.class, args); } /** * 检查 Jasypt 的密钥设置情况,若未通过命令行传递,则设置默认值 * @param cmdLineArgs 命令行的参数 * @return 经过检查后的参数数组 */ private static String[] checkOrSetDefaultJasyptPassword(String[] cmdLineArgs) { String defaultJasyptPasswordProperty = "--jasypt.encryptor.password=cnblogs"; if (cmdLineArgs.length == 0) { return new String[]{defaultJasyptPasswordProperty}; } if (isCmdLineArgsContainsJasyptPassword(cmdLineArgs)) { return cmdLineArgs; } String[] enhancedArgs = new String[cmdLineArgs.length + 1]; for (int i = 0; i < cmdLineArgs.length; i++) { enhancedArgs[i] = cmdLineArgs[i]; } enhancedArgs[enhancedArgs.length - 1] = defaultJasyptPasswordProperty; return enhancedArgs; } private static boolean isCmdLineArgsContainsJasyptPassword(String[] args) { for (String arg : args) { if (arg.startsWith("----jasypt.encryptor.password=")) { return true; } } return false; } @Override public void run(String... args) throws Exception { // 这两个配置,本身就是明文的 System.out.println("demo-app.username = " + env.getProperty("demo-app.username")); System.out.println("demo-app.password = " + env.getProperty("demo-app.password")); // 以下两个属性配置项,文件为密文,但PropertySource加载后经过jasypt的处理,变成了明文 System.out.println("database.username = " + env.getProperty("database.username")); System.out.println("database.password = " + env.getProperty("database.password")); } }
代码说明一:
上述代码的 ① ② 处,您一定会有疑问: 这个密文是如何生成的呢,Jasypt 如何能在项目启动阶段对其正确的解密呢?没错,实际上这里的密文就是提前用 Jasypt 加密生成出来的。必须要保证生成密文所用的 password 与 SpringBoot 项目中给 Jasypt 指定的 password 内容是一致的,才能解密成功。下面是一段简单的加密代码示例:
public static String encrypt(String plainText, String secretKey) { BasicTextEncryptor textEncryptor = new BasicTextEncryptor(); textEncryptor.setPassword(secretKey); return textEncryptor.encrypt(plainText); }
代码说明二:
代码 ③ 处,在正式执行 SpringApplication.run(....) 方法之前, 对命令行参数 args 进行了检查,并使用了检查后的 args 作为命令行参数传递给 SpringApplication.run() 方法。对 args 参数的检查逻辑为:判断 args 中是否包含 --jasypt.encryptor.password= 的配置,如果没有,则默认追加上该条目,并给出一个默认的password。
这一步实际上就是希望将 Jasypt 的密钥通过命令行参数传递过来,而不是配置在配置文件中,避免可以通过配置中心的管理页面,直接查看到该密钥。如何保管好密钥,这是一个纯管理问题了,技术上只要能支撑起设计好的管理方式即可。
本小节的完整代码已上传到 jasypt-spring-boot-demo
28. 如何处理同名Bean对象多次注册导致的启动失败问题
这里单指不能修改程序包的情况,如果能修改源码,该问题自然好处理了,在代码中避免注册同名的Bean即可。以下情况,往往是无法修改源码的
-
从其它团队交接过来的项目,且只有jar包,没有源码
至于为什么交接的项目没有源码,国内的公司管理情况你懂的 😁 ,尤其是那种交接了好几手的项目,部分工程源码丢失也不是没有可能的 -
项目中嵌套引用的基础工具
这些工具以AutoConfiguration的方式做成Jar包,在其它工程中引入时,再次被包装成上层工具,这种层层包装方式,极有可能导致同名组件注册 -
引入的第三方包中有同名组件注册问题
解决办法是,直接允许同名组件多次注册,配置如下:
spring.main.allow-bean-definition-overriding = true
关于 spring.main 的更多配置参见 SpringBoot项目组件常见配置
29. 如何优雅地停止 SpringBoot 服务
29.1 优雅停止不涉及 Web 服务的 SpringBoot 项目
通常来说,如果 SpringBoot 项目不涉及 Web 服务,但它还长时间在运行,那程序中一定有任务执行器在执行周期性任务。因此,优雅停机的方式就是要调用所有任务执行器的 shutdown 方法,该方法会让任务执行器进入停止状态,此时它具有以下特性:
- 执行器不再接收新任务
- 执行器等待已提交任务的执行完成
- 超过最大等待时长任务依然没有执行完,则强制结束
示例代码如下:
先装配一个任务执行器
@Bean("taskExecutor") public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(2); taskExecutor.setMaxPoolSize(2); // 开启「停机时等待已提交任务的执行完成」特性 taskExecutor.setWaitForTasksToCompleteOnShutdown(true); // 设置最大等待时长 taskExecutor.setAwaitTerminationSeconds(120); // 初始化任务执行器,此后执行器就进入工作状态了 taskExecutor.initialize(); return taskExecutor; }
然后利用 Spring 生命周期中的 Bean 销毁回调,触发执行器 shutdown 方法的调用
@Component public class ExecutorShutdownHook implements DisposableBean { @Resource ThreadPoolTaskExecutor taskExecutor; // 进程结束前,会调用本方法 @Override public void destroy() throws Exception { System.out.println("[ShutdownHook]: 开始停止任务执行器"); taskExecutor.shutdown(); System.out.println("[ShutdownHook]: 任务执行器已平滑停止"); } }
29.2 优雅停止包含 Web 服务的 SpringBoot 项目
对于涉及 Web 服务的 SpringBoot 项目,与 nginx 一样,先让 Servlet 容器停止接收新请求,待已接收的请求处理完毕后,执行业务服务本身的清理工作。最后停止Servlet容器,结果整个服务进程。
在早期,上述工作需要开发人员,结合所用的 Servelt 容器(如 Tomcat)所提供的接口能力,手动编码来完成。从 spring-boot 2.3
开始,官方引入了 server.shutdown
这个配置项,只需要将其设置为 graceful 即可。即当配置了 server.shutdown=graceful
时,程序就能优雅停止 Web 服务了。与任务执行器的优雅停止一样,等待已接收请求的处理完成,也有个最大时长,这个时长也可以在 properties 中配置。如下所示:
server: # 开启Web服务的优雅停止特性 shutdown: graceful spring: lifecycle: # 设置收到TERM信号后,等待完成的最大时长 timeout-per-shutdown-phase: 2m
下面是一个同时包含了「任务执行器」和「Web服务」的SpringBoot样例项目,平滑停机的最后日志输出:
[任务-1]: 我开始睡觉了哈... [任务-2]: 我开始睡觉了哈... 2024-03-27 11:36:21.617 INFO 26540 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2024-03-27 11:36:21.617 INFO 26540 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2024-03-27 11:36:21.618 INFO 26540 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms [SampleController#justRequest]: 将于10秒后返回 2024-03-27 11:36:25.909 INFO 26540 --- [ionShutdownHook] o.s.b.w.e.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete [SampleController#justRequest]: 10秒时间到了,返回给客户端 2024-03-27 11:36:31.768 INFO 26540 --- [tomcat-shutdown] o.s.b.w.e.tomcat.GracefulShutdown : Graceful shutdown complete [ShutdownHook]: 开始停止任务执行器 [任务-1]: 我已经睡醒了喔 ^_^ [任务-2]: 我已经睡醒了喔 ^_^ [任务-3]: 我开始睡觉了哈... [任务-3]: 我已经睡醒了喔 ^_^ [ShutdownHook]: 任务执行器已平滑停止
在第 6 行时,我们通过浏览器访问了 http://localhost:8080/test
,这个请求会进入到 SampleController 的 test 方法,方法故意 sleep 了10秒,因此浏览器端处于等等响应的过程。页面没有输出。
在第 7 行处,向进程发出了终止信号,该行日志表明: 整个程序在等等活跃请求(active requests 即已接收到的请求)的执行完成。而此时第 6 行的Web请求依然还在处理中,直到 SampleController 的 test 方法 sleep 结束,才返回给了浏览器端,同时 Servelt 容器完全停止(见第 9 行)。
后面的 [任务-]、[任务-2]、[任务-3] 是任务执行器的平滑停机过程,这个是需要我们手动编码来控制的。
整个优雅停机样例的源码已上传到 gracefully-shutdown-spring-boot
30. 如何处理 YAML 或 Properties 的解析异常 MalformedInputException
时常遇到在IDE中启动程序OK,但打包后使用命令启动则抛出类似这面这样的错误:
org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 1
解决文案分两步:
-
修正文件编码
-
设置java命令的编码参数
如:java -Dfile.encoding=utf8 -jar xxxx.jar
31. 如何在运行期动态调整日志级别和增减Logger
可以在logback的配置文件中,指定日志配置的刷新周期,程序在运行期会按这个周期重要加载日志输出配置,如下所示:
<?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" scanPeriod="300 seconds">  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
32. IntelliJ Idea 中代码飘红的常见解决步骤
按照以下顺序进行,如果前一步执行完后问题已解决,就不用尝试后面的办法了。一般而言最后一步是终极大招,若这一招也不灵,那只能乞求上帝了。
- 重新导入
- 重建idea 文件索引缓存
- 删除工程目录下的 .idea/ 目录
33. 一份 Linux 环境下部署 SpringBoot 程序的参考 Shell 脚本
本脚本提供以下特性
- 关闭终端后,Java进程不会退出
- 提供了部署、启动、停止、重启三种最见的操作
- 每次部署时,均备份当前的程序包(可通过参数关闭备份)
Shell部署脚本
#!/bin/bash THIS_DIR=$(cd $(dirname $0);pwd) # 要部署的服务名称,请根据实际情况修改 SERVICE_NAME=gateway-service # 要部署的服务名Jar包名称,请根据实际情况修改 JAR=gateway-service.jar CONFIG_FILE=${THIS_DIR}/application.yml usage() { # 提示内容请根据实际情况修改 echo "网关服务" echo "用法: startup.sh [deploy|start|stop|status]" echo " deploy: 部署新的jar包, 动作有" echo " · 备份当前正运行的jar包" echo " · 替换正运行的jar包" echo " · 停止当前服务进程" echo " · 使用jar包启动服务程序" echo " · 启动成功,则只保留最近10个部署包" echo "" echo " start: 直接启动程序包,如果已存在服务进程,会启动失败" echo "" echo " stop: 停止服务进程" echo "" echo " status: 检查服务进程是否处于运行状态" echo "" exit 0 } check_process_existance(){ pid=`ps -ef | grep $JAR | grep -v grep | awk '{print $2}'` if [ -z "$pid" ]; then return 1 else return 0 fi } run(){ OPTS="-server -Dfile.encoding=utf8 -Dsun.jnu.encoding=utf8 -Xms3g -Xmx3g -Xmn2g -XX:SurvivorRatio=9" OPTS="${OPTS} -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDateStamps -XX:+PrintReferenceGC" OPTS="${OPTS} -XX:+PrintTenuringDistribution -XX:+PrintHeapAtGC -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime" OPTS="${OPTS} -Xloggc:$THIS_DIR/logs/gc/gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M" OPTS="${OPTS} -XX:-UseAdaptiveSizePolicy -XX:+PrintAdaptiveSizePolicy -XX:ErrorFile=$THIS_DIR/hs_err_license-service_%p.log" CMD="java ${OPTS} -jar $THIS_DIR/$JAR " RUN_PARAMS=" --spring.config.location=${CONFIG_FILE}" echo -e "Start ${SERVICE_NAME} with command:\n\t${CMD} ${RUN_PARAMS}" nohup $CMD $RUN_PARAMS > /dev/null 2>&1 & echo "$CMD $RUN_PARAMS" # 下面这个命令是将java程序在前台运行,以便在启动失败时,查看详细原因 # 如果使用该命令,在终端前台也找不到有用的错误信息的话,需要个性 ${THIS_DIR}/logback.xml的日志输出器 # 生产环境下,log方式的日志不输出到控制台, 只有 System.out 和 System.error 的打印才输出到终端 #$CMD $RUN_PARAMS } deploy() { echo "deploy命令功能尚未完成" exit 1 } start(){ check_process_existance if [ $? -eq "0" ];then echo "服务 ${SERVICE_NAME} 正处于运行中,进程号为:${pid} "; else run fi } stop(){ check_process_existance if [ $? -eq "0" ];then echo "Stopping ${SERVICE_NAME} with pid: ${pid}" kill $pid while [ -e /proc/${pid} ]; do sleep 1; done echo "Shutdown ${SERVICE_NAME} with pid: ${pid}" else echo "${SERVICE_NAME} is not running" fi } status(){ check_process_existance if [ $? -eq "0" ];then echo "${SERVICE_NAME} is running, pid is ${pid}" else echo "${SERVICE_NAME} is not running." fi } case "$1" in "deploy") deploy ;; "start") start ;; "stop") stop ;; "status") status ;; *) usage ;; esac
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验