springboot踩坑记
1. @ConditionalOnProperty 根据配置加载不同的 bean
场景:对 redis 配置进行封装,实现自动化配置,能兼容哨兵模式和集群模式。
想到在 redis 配置中加一个 redis.type 来区分集群和哨兵模式(redis.type=cluster 或 sentinel),然后根据 type 来分别加载 JedisConnectionFactory、RedisClusterConfiguration、RedisSentinelConfiguration
配置如下:但是一直不成功,报没有 JedisConnectionFactory 这个 bean
@ConditionalOnProperty(name = "redis.type", havingValue = "cluster")
@ConditionalOnMissingBean
@Bean
public RedisClusterConfiguration redisClusterConfiguration() {
List<RedisNode> nodeList = new ArrayList<>();
String[] cNodes = hostName.split(",");
//分割出集群节点
for (String node : cNodes) {
String[] hp = node.split(":");
nodeList.add(new RedisNode(hp[0], Integer.parseInt(hp[1])));
}
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
redisClusterConfiguration.setClusterNodes(nodeList);
redisClusterConfiguration.setMaxRedirects(maxRedirects);
return redisClusterConfiguration;
} @ConditionalOnProperty(name = "redis.type", havingValue = "cluster")
@ConditionalOnMissingBean
@Bean
public JedisConnectionFactory jedisConnectionFactory(JedisPoolConfig jedisPoolConfig, RedisClusterConfiguration redisClusterConfiguration) {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisClusterConfiguration, jedisPoolConfig);
jedisConnectionFactory.setTimeout(timeout);
jedisConnectionFactory.setPassword(password);
return jedisConnectionFactory;
} @ConditionalOnProperty(name = "redis.type", havingValue = "sentinel")
@ConditionalOnMissingBean
@Bean
public RedisSentinelConfiguration redisSentinelConfiguration() {
Set<String> sentinelHostAndPorts = Sets.newHashSet(Splitter.on(",").split(hostName).iterator());
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(masterName, sentinelHostAndPorts);
return redisSentinelConfiguration;
} @ConditionalOnProperty(name = "redis.type", havingValue = "sentinel")
@ConditionalOnMissingBean
@Bean
public JedisConnectionFactory jedisConnectionFactory(JedisPoolConfig jedisPoolConfig, RedisSentinelConfiguration redisSentinelConfiguration) {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisSentinelConfiguration, jedisPoolConfig);
jedisConnectionFactory.setTimeout(timeout);
jedisConnectionFactory.setPassword(password);
return jedisConnectionFactory;
}
后来发现,将两个定义 JedisConnectionFactory 的方法改成不相同才可以,原因暂时不详
@ConditionalOnProperty(name = "redis.type", havingValue = "cluster")
@ConditionalOnMissingBean
@Bean
public RedisClusterConfiguration redisClusterConfiguration() {
List<RedisNode> nodeList = new ArrayList<>();
String[] cNodes = hostName.split(",");
//分割出集群节点
for (String node : cNodes) {
String[] hp = node.split(":");
nodeList.add(new RedisNode(hp[0], Integer.parseInt(hp[1])));
}
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
redisClusterConfiguration.setClusterNodes(nodeList);
redisClusterConfiguration.setMaxRedirects(maxRedirects);
return redisClusterConfiguration;
} @ConditionalOnProperty(name = "redis.type", havingValue = "cluster")
@ConditionalOnMissingBean
@Bean
public JedisConnectionFactory jedisConnectionFactory1(JedisPoolConfig jedisPoolConfig, RedisClusterConfiguration redisClusterConfiguration) {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisClusterConfiguration, jedisPoolConfig);
jedisConnectionFactory.setTimeout(timeout);
jedisConnectionFactory.setPassword(password);
return jedisConnectionFactory;
} @ConditionalOnProperty(name = "redis.type", havingValue = "sentinel")
@ConditionalOnMissingBean
@Bean
public RedisSentinelConfiguration redisSentinelConfiguration() {
Set<String> sentinelHostAndPorts = Sets.newHashSet(Splitter.on(",").split(hostName).iterator());
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(masterName, sentinelHostAndPorts);
return redisSentinelConfiguration;
} @ConditionalOnProperty(name = "redis.type", havingValue = "sentinel")
@ConditionalOnMissingBean
@Bean
public JedisConnectionFactory jedisConnectionFactory2(JedisPoolConfig jedisPoolConfig, RedisSentinelConfiguration redisSentinelConfiguration) {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisSentinelConfiguration, jedisPoolConfig);
jedisConnectionFactory.setTimeout(timeout);
jedisConnectionFactory.setPassword(password);
return jedisConnectionFactory;
}
2. SpringBoot 启动程拉起了两个 spring 容器,且父容器不受使用者控制
SpringBoot 应用在启动时,一般只会启动一个 spring 容器(AnnotationConfigServletWebServerApplicationContext);
但是当项目中引用了 spring-cloud-context-xxx.jar 时,默认会启动两个 spring 容器。AnnotationConfigApplicationContext(父)、AnnotationConfigServletWebServerApplicationContext(子, parent 指向父)
这是由于 spring-cloud-context-xxx.jar 中的 BootstrapApplicationListener 会再启动一个容器
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); // 准备环境
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context); // 刷新上下文
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
1. prepareEnvironment(listeners, applicationArguments)
2. EventPublishingRunListener#environmentPrepared(environment)
3. SimpleApplicationEventMulticaster#multicastEvent()
先获取 getApplicationListeners
0 = {BootstrapApplicationListener@2536} // 会再启动一个容器
1 = {LoggingSystemShutdownListener@2628}
2 = {ConfigFileApplicationListener@2629}
3 = {AnsiOutputApplicationListener@2630}
4 = {LoggingApplicationListener@2631}
5 = {ClasspathLoggingApplicationListener@2632}
6 = {BackgroundPreinitializer@2633}
7 = {DelegatingApplicationListener@2634}
8 = {FileEncodingApplicationListener@2635}
--> 循环调用 ApplicationListener.onApplicationEvent(event);
这样带来了一个问题,我们无法控制 BootstrapApplicationListener 拉起的容器。
场景:
springboot 与 disconf 整合时,项目中同时引入了 spring-cloud-context-xxx.jar, spring-boot-autoconfigure-xxx.jar,spring-boot-autoconfigure-xxx.jar 的 spring.factories 文件中会自动配置 PropertyPlaceholderAutoConfiguration,它注册了一个 bean : PropertySourcesPlaceholderConfigurer。
PropertySourcesPlaceholderConfigurer 默认情况下解析配置占位符取不到结果时会报错(ignoreUnresolvablePlaceholders = false),而且它的优先级最高。导致 disconf 自定义的 PlaceholderConfigurer 无法执行解析就已经报错了。
最容易想到的办法就是排除掉自动注册的 PropertySourcesPlaceholderConfigurer,但是使用 @SpringBootApplication(exclude = PropertyPlaceholderAutoConfiguration.class) 不起作用,因为自动注册的 PropertySourcesPlaceholderConfigurer 是在父容器里面,而项目的启动类拉起的是子容器,导致这个 bean 无法被排除。
解决办法:
我们可以设置 spring.cloud.bootstrap.enabled=false 来禁止 BootstrapApplicationListener 启动新的 spring 容器
3. spring.config.location 的功能在 1.5.9 和 2.2.0 版本下有不同的表现
场景:
在开发阶段,application.yml 存放在 classpath 下,上测试或生产环境时,将配置文件外部化,aplication.yml 存放在与 jar 同级的 conf/ 目录下。
SpringBoot 2.2.0 版本下指定 --spring.config.location=conf/ 时,只会加载 conf/目录下的 application.yml,而 SpringBoot 1.5.9 版本下,会加载 conf/ 目录下和 jar 包里面 classpath 下的 application.yml,导致 jar 包里面的本地开发配置也被加载了
原因:
SpringBoot 1.5.9 与 2.2.0 版本的外部化配置 spring.config.location 逻辑不相同
ConfigFileApplicationListener.Loader#getSearchLocations()
v1.5.9: 将指定的搜索路径添加到默认的搜索路径(classpath:/,classpath:/config/,file:./,file:./config/)中
v2.2.0: 直接使用指定搜索路径下的配置文件 
4. 使用 SpringBoot 2.2.2Release + MyBatisPlus 3.1.1 操作数据库时,更新时间有时不准确,有时差问题
场景:
数据库中有一条数据,使用 selectById 将它查出来,再使用 udpateById 进行更新(所有非空字段都会更新),将 SQL 打印出来时,会发现日期更新会出错,偏差 13 h
SpringBoot 2.2.2Release 依赖的 MySQL 驱动是 mysql:mysql-connector-java:8.0.18 ,对应的驱动类是 com.mysql.cj.jdbc.Driver,连接串如果不设置时区,很有可能有时区问题
时间在转换时出了问题,实体使用的是 LocalDateTime 类,不知道使用 Date 类会不会有问题,有待验证
解决办法:
将 DB 连接串加上时区serverTimezone=GMT%2B8(北京时间东八区)
jdbc:mysql://localhost:3306/mydb3?serverTimezone=GMT%2B8&useSSL=false
参考:https://blog.csdn.net/love20yh/article/details/80799610
https://blog.csdn.net/m0_37997811/article/details/97113252 (有可能设置 mysql 服务器的时区也可以,但是改连接串更好)
https://juejin.im/post/5902e087da2f60005df05c3d
值得注意的是:
经测试发现, insert 时,时间是没有问题的(没有带时间插入,让数据库自动填充的);更新数据时,如果不带时间,让数据库自动更新 update_time 字段,也是没有问题的。只有带时间更新时,java 类中的时间对象转成 sql 语句中的时间时,才出问题
DDL 中时间的设置为:
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
其实,如果只是更新创建修改时间的话,完全没有必要带时间更新,让数据库帮我们来完成即可规避这个问题了。如果是其他时间,就必须要用上面的方法来解决了
5. 调整 bean 的加载顺序
调整 bean 的加载顺序的方式有很多种:
1. 可以通过 @Autowired 的方式,让一个 bean 进行初始化
例如: ServiceA 加载之前,一定要初始化 ServiceB,则可以将 ServiceB 通过 @Autowired 时行注入,让 ServiceB 在 ServiceA 之前时行初始化
2. @ConditionalOnBean(xxx)
springboot踩坑记的更多相关文章
- SpringBoot踩坑记(HTTP 400 错误)
HTTP 400 错误 复现错误 ajax请求后台数据时有时会报 HTTP 400 错误 - 请求无效 (Bad request);出现这个请求无效报错说明请求没有进入到后台服务里:原因:1)前端提交 ...
- 记一次 Spring 事务配置踩坑记
记一次 Spring 事务配置踩坑记 问题描述:(SpringBoot + MyBatisPlus) 业务逻辑伪代码如下.理论上,插入数据 t1 后,xxService.getXxx() 方法的查询条 ...
- Spark踩坑记——Spark Streaming+Kafka
[TOC] 前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark strea ...
- Spark踩坑记——数据库(Hbase+Mysql)
[TOC] 前言 在使用Spark Streaming的过程中对于计算产生结果的进行持久化时,我们往往需要操作数据库,去统计或者改变一些值.最近一个实时消费者处理任务,在使用spark streami ...
- 【踩坑记】从HybridApp到ReactNative
前言 随着移动互联网的兴起,Webapp开始大行其道.大概在15年下半年的时候我接触到了HybridApp.因为当时还没毕业嘛,所以并不清楚自己未来的方向,所以就投入了HybridApp的怀抱. Hy ...
- Spark踩坑记——共享变量
[TOC] 前言 Spark踩坑记--初试 Spark踩坑记--数据库(Hbase+Mysql) Spark踩坑记--Spark Streaming+kafka应用及调优 在前面总结的几篇spark踩 ...
- Spark踩坑记——从RDD看集群调度
[TOC] 前言 在Spark的使用中,性能的调优配置过程中,查阅了很多资料,之前自己总结过两篇小博文Spark踩坑记--初试和Spark踩坑记--数据库(Hbase+Mysql),第一篇概况的归纳了 ...
- djangorestframework+vue-cli+axios,为axios添加token作为headers踩坑记
情况是这样的,项目用的restful规范,后端用的django+djangorestframework,前端用的vue-cli框架+webpack,前端与后端交互用的axios,然后再用户登录之后,a ...
- HttpWebRequest 改为 HttpClient 踩坑记-请求头设置
HttpWebRequest 改为 HttpClient 踩坑记-请求头设置 Intro 这两天改了一个项目,原来的项目是.net framework 项目,里面处理 HTTP 请求使用的是 WebR ...
随机推荐
- 闲谈关于discuz内核缓存机制
Discuz! 缓存 Discuz! X2.5 的 config_global.php 中有这样一行代码 $_config['cache']['type'] = 'sql'; 这就是 Discuz! ...
- wordpress时间函数the_time() 实例解读
wordpress the_time()时间函数想必大家多多少少都会用到,但是要自定义一些时间相对没那么熟悉了,随ytkah一起来看看吧.我们知道时间函数基础调用是<?php the_time( ...
- PHP数组操作类
class ArrayHelper{ /** * 从数组中删除空白的元素(包括只有空白字符的元素) * * 用法: * @code php ...
- Spring Boot 2.x(十六):玩转vue文件上传
为什么使用Vue-Simple-Uploader 最近用到了Vue + Spring Boot来完成文件上传的操作,踩了一些坑,对比了一些Vue的组件,发现了一个很好用的组件--Vue-Simple- ...
- ajax jsonp函数调用
jsonp数据 jsonpHandler({name:"liujinyu",age:"24"}) ajax调用 $.ajax({ type:'GET', ...
- can总线学习网上资料汇总
https://zhuanlan.zhihu.com/p/30247549?utm_source=wechat_session&utm_medium=social&utm_oi=280 ...
- 05_配置交换机SSH服务(数通华为)
1. 网络拓扑: 2. SW1配置:2.1 配置为Access口,vlan 10:[SW1]vlan 10[SW1-GigabitEthernet0/0/1]port link-type access ...
- Selenium元素定位的几种方式
一.通过id查找 例:<input id="kw" name="wd" class="s_ipt" value="" ...
- [RN] React Native 实现 类似京东 的 沉浸式状态栏和搜索栏
React Native 实现 类似京东 的 沉浸式状态栏和搜索栏 其原理其实就是在要 隐藏 部分的那个View 前面加入 StatusBar 代码! 代码如下: <StatusBar anim ...
- 59: loj #10215
$des$ https://loj.ac/problem/10215 $sol$ exgcd检查 $code$ #include <iostream> #include <cstdl ...