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踩坑记的更多相关文章

  1. SpringBoot踩坑记(HTTP 400 错误)

    HTTP 400 错误 复现错误 ajax请求后台数据时有时会报 HTTP 400 错误 - 请求无效 (Bad request);出现这个请求无效报错说明请求没有进入到后台服务里:原因:1)前端提交 ...

  2. 记一次 Spring 事务配置踩坑记

    记一次 Spring 事务配置踩坑记 问题描述:(SpringBoot + MyBatisPlus) 业务逻辑伪代码如下.理论上,插入数据 t1 后,xxService.getXxx() 方法的查询条 ...

  3. Spark踩坑记——Spark Streaming+Kafka

    [TOC] 前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark strea ...

  4. Spark踩坑记——数据库(Hbase+Mysql)

    [TOC] 前言 在使用Spark Streaming的过程中对于计算产生结果的进行持久化时,我们往往需要操作数据库,去统计或者改变一些值.最近一个实时消费者处理任务,在使用spark streami ...

  5. 【踩坑记】从HybridApp到ReactNative

    前言 随着移动互联网的兴起,Webapp开始大行其道.大概在15年下半年的时候我接触到了HybridApp.因为当时还没毕业嘛,所以并不清楚自己未来的方向,所以就投入了HybridApp的怀抱. Hy ...

  6. Spark踩坑记——共享变量

    [TOC] 前言 Spark踩坑记--初试 Spark踩坑记--数据库(Hbase+Mysql) Spark踩坑记--Spark Streaming+kafka应用及调优 在前面总结的几篇spark踩 ...

  7. Spark踩坑记——从RDD看集群调度

    [TOC] 前言 在Spark的使用中,性能的调优配置过程中,查阅了很多资料,之前自己总结过两篇小博文Spark踩坑记--初试和Spark踩坑记--数据库(Hbase+Mysql),第一篇概况的归纳了 ...

  8. djangorestframework+vue-cli+axios,为axios添加token作为headers踩坑记

    情况是这样的,项目用的restful规范,后端用的django+djangorestframework,前端用的vue-cli框架+webpack,前端与后端交互用的axios,然后再用户登录之后,a ...

  9. HttpWebRequest 改为 HttpClient 踩坑记-请求头设置

    HttpWebRequest 改为 HttpClient 踩坑记-请求头设置 Intro 这两天改了一个项目,原来的项目是.net framework 项目,里面处理 HTTP 请求使用的是 WebR ...

随机推荐

  1. Locust性能测试6-命令行参数详解

    前言 当我们在linux上使用locust工具压测的时候,会使用no-web模式,然后需要收集运行的日志,方便查找问题. 命令行参数 输入locust --help 查看所有的命令行参数 > l ...

  2. DateFormat与SimpleDateFormat区别和使用详解

    DateFormat类 此类是一个日期的格式化类,用来格式化日期.具体日期可以通过java.util.Date类来获取. DateFormat类的定义:此类是定义在java.test包中的. publ ...

  3. 原生js的常用方法总结

    =============== 通知: 博主已迁至<掘金>码字,博客园可能以后不再更新,掘金地址:https://juejin.im/post/5a1a6a6551882534af25a8 ...

  4. 利用Java反射机制优化简单工厂设计模式

    之前项目有个需求,审批流程的时候要根据配置发送信息:发送短信.发送邮件.当时看到这个就想到要用工厂模式,为什么要用工厂模式呢?用工厂模式进行大型项目的开发,可以很好的进行项目并行开发.就是一个程序员和 ...

  5. Linux—— 报错汇总

    前言 记录Linux相关的错误问题和解决方法 问题 tar: Error is not recoverable: exiting now [报错] tar -zxvf mysql-server_5.6 ...

  6. 【Postgres】Windows2012 不能启动

    PG启动错误-不知道什么错误 等待 postgresql-x64- 服务的连接超时( 毫秒). 由于下列错误,postgresql-x64- 服务启动失败: 服务没有及时响应启动或控制请求. 编写ba ...

  7. input提示字在有焦点消失或输入改变时消失

    一:获取焦点时 提示字消失 <input type="text" name="textfield" value="这里是提示内容" o ...

  8. hdu3974-Assign the task-(dfs+线段树)

    题意:有n个人,有上下级关系,有m个操作,有两种操作1.把一个任务分给某个人,他的下属也会停下手中工作和他一起做:2.查询某个人的当前任务是什么? 解题:n-1个关系,总有一个人没有上级,以他为根节点 ...

  9. 开发(一) ardunio环境配置 针对esp32-cam 更多例程

    第一种  简单版本,针对获取mpu9250数据, http://www.bubuko.com/infodetail-3093785.html 第二种 浮渣版本,针对ESP32获取图像,以及跟多开发例程 ...

  10. VGG梳理

    创新点(小卷积核.小池化核.层数更深.全连接变卷积) 对AlexNet改进,在第一个卷积层中使用了更小的卷积核和卷积stride 多尺度(训练和测试时,采用图片的不同尺度(当然是按各向同性缩放的最小边 ...