3

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

 1 month ago
source link: https://www.cnblogs.com/guzb/p/spring-boot-common-development-issue-solution-list.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

  • https://mvnrepository.com/

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

  • https://central.sonatype.com/

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

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

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

进入SpringBoot参考手册页面
点击 Quick Start
查看 System Requirement

重大版本与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; @Componentpublic 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")@Configurationpublic 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

@Configurationpublic 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@Componentpublic 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

@Mapperpublic 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

@Mapperpublic 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

@Configurationpublic 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@SpringBootApplicationpublic 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 服务器的组件配置类

@Configurationpublic 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@SpringBootApplicationpublic 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(点击查看)

@Componentpublic 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

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

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

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

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

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

下面是一段示例代码

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

📣 特别说明

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

@EnableAsyncpublic class BookService { @Async("bookExcutor") // 在注解中指定执行器  ̄ ̄ ̄ ̄ ̄ ̄ ̄ public void disableAllExpiredBooks(){ .... }} @Configurationpublic 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. 如何统一处理异常

  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.ResponseEntityimport org.springframework.http.HttpStatus; @RestControllerAdvicepublic 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. 如何解决同一实例内部方法调用时,部分事务失效的问题

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

@Servicepublic 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. 如何阻止某个第三方组件的自动装配

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

    如下代码所示:

    // 启动时,将Spring官方的数据源自动装配排除@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄public class MyAppMain{ public static void main(String[] args) { SpringApplication.run(MyAppMain.class, args); }}
  2. 方法二:在配置文件中指定 <推荐>

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

    spring: autoconfigure: # 指定要排除的自动装配类,多个类使用英文逗号分隔 exclude: org.springframework.cloud.gateway.config.GatewayAutoConfiguration
  3. 方法三:临时注释掉该组件的 @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&regionLevel=COUNTY  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄

    默认情况下,Query 与 Path Variable 参数验证是没有效果的。可通过以下步骤开启该类型的参数校验:

    • 提供一个 MethodValidationPostProcessor 来处理 @Validated 注解。
    • 在 Controller 类上添加 @Validated 注解
      ⚠️ 注意:将 @Validate 注解添加在 Controller 的方法上或方法的参数上,均不会使 MethodValidationPostProcessor 生效,也就不能执行Query与PathVariable参数的校验
    示例代码(点击查看)
    @Configurationpublic 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);}

📣 特别说明

  • 需要将 JSR-303 的api及相应实现添加到 classpath。当我们引入SpringMvc时,它们也自动引入了
  • @Valid 是 JSR-303 的标准注解
  • @Validated 是 Spring 的验证框架注解,它完全兼容 @Valid
  • MethodValidationPostProcessor 是 Spring 的验证框架中的一个容器Bean后置处理器

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; @Componentpublic 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; @Componentpublic 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; @Componentpublic 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; @Componentpublic 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; @Componentpublic class OnceTask implements CommandLineRunner { // 监听的事件还可以换成 ApplicationReadyEvent @Override public void run(String... args) throws Exception { System.out.println("我将在应用容器完成所有Bean的装配后执行"); }}

📣 特别说明

严格说来,事件监听方式和 CommandLineRunner 方式才是最符合要求的,它才是真正意义上的「容器启动完成后」执行的方法。另外两种方式实际上是在「Bean 初始化完成后」执行的。

假定有一个类,集中使用了上面的所有方式,那么这些方法谁先执行谁后执行呢 ?看看下面这个例子:

示例代码(点击查看)

@Comonentpublic 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"); }}

以上代码的执行结果为:

[email protected]()OnceTask.onStartedByInterface(ApplicationStartedEvent)OnceTask.@EventListener(ApplicationStartedEvent)OnceTask.commandLineRunnerOnceTask.@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 相互依赖对方,导致容器启动失败@Servicepublic class ServiceA { @Autowired private ServiceB serviceB;} @Servicepublic class ServiceB { @Autowired private ServiceA serviceA;}

通过手动装配部分依赖,解决循环依赖问题(点击查看)

@Servicepublic 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 正常装配@Servicepublic class ServiceB { @Autowired private ServiceA serviceA;}

26. 如何在所有Web请求的前后执行自己的代码

采用以下两种方式中的一种即可

  1. Sevlet 过滤器

  2. SpringMvc的HandlerInterceptor接口

27. 如何统一给配置项属性值加密

一般说来,研发人员是接触不到生产环境中的配置文件的,正规的项目,也不会将生产环境的信息内置到源代码Jar包中。因此,多数 C 端的项目是不需要对配置文件进行加密的。有此要求的大多是 toB 或 toG 类项目。

统一给配置文件中的指定属性值加密,可以使用 Jasypt 来完成。Jasypt 原本只是一个加密解密的基础工具,但经过进一步封装增强后的 jasypt-spring-boot-starter,便能够统一地对 SpringBoot 项目中的加密配置属性进行解密。全程是自动的,默认情况下,只需要进行以下两步设置:

  1. 设置 jasypt 的加密密钥

    Jasypt 默认的加密类的是 SimpleAsymmetricStringEncryptor, 它的密钥取自属性 jasypt.encryptor.password

  2. 生成密文并标识其需要由 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

在主方法中验证

点击查看代码

@SpringBootApplicationpublic 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 方法的调用

@Componentpublic 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

解决文案分两步:

  1. 修正文件编码

  2. 设置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.jarCONFIG_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 -Xms128m -Xmx256m -Xmn256m" OPTS="${OPTS} -XX:HeapDumpPath=$THIS_DIR/vmstack/heap.dump -XX:+PrintGCDetails" OPTS="${OPTS} -XX:-HeapDumpOnOutOfMemoryError -XX:ErrorFile=$THIS_DIR/vmstack/hs_err.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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK