GoLang读取yaml配置文件

最近这段时间在用GoLang写项目。其中如何读取配置文件一度是让我比较头疼的事情,好在最后都解决了,这里简单记录一下GoLang读取配置文件的方式。

因为写了多年java springboot项目的缘故,在多种配置方式中独独比较偏爱yaml的格式。因此为新项目选择配置文件的时候就毫不犹豫地选择了继续使用yaml。

使用yaml时遇到了这样几个问题:

  1. yaml配置文件读取并映射为struct
  2. yaml多配置文件读取
  3. 适应性选择配置

接下来就以这几个问题的顺序来简单介绍下使用GoLang如何读取yaml配置文件以及关于配置文件选择的问题。

读取yaml配置文件

也许很多人都在网上搜索过“golang读取yaml”这样的词条,搜索结果也通常大部分都是推荐使用gopkg.in/yaml这个package。

gopkg.in/yaml诚然是读取yaml最经典的一个方案,但却不是最简单易用的一个方案。

读取yaml(或其它类型的配置文件)我首先推荐使用viper,因为它用起来最简洁。

来看一个viper读取配置文件的示例:

到这里配置文件的读取就已经完成了。

使用配置信息时可以使用viperGetIntGetStringGetBool等方法来读取。

比如类似下面的配置信息:

要获取配置中的age信息时可以这样做:

viper也支持将配置信息渲染为struct对象:

需要注意一点:viper并不是直接将yaml配置文本转换成了struct对象。viper是先将配置信息转换成了map结构,而后再通过github.com/mitchellh/mapstructure这个模块将map对象转换为struct对象。因此在配置struct成员映射关系时使用的是mapstructure关键字,而非yaml关键字。

yaml多配置文件读取

在java springboot中可以做这样的配置:在application.yml做通用的配置;在application-dev.yml和application-prod.yml添加适应不同环境的个性化配置;如有相同配置项,后者可以覆盖前者。

我非常喜欢这种配置方法,能够灵活适配不同环境,减少冗余配置。但很不幸,viper目前并不支持类似的操作。头疼了一段时间后,干脆参考viper的代码写了一个新的项目vibe。这个项目目前的版本是0.0.5。在读取yaml文件这一点上,vibe的使用方式和viper几乎是一样的——除了能够满足读取多配置文件这一项。

看下示例代码:

这里只看第一行代码就好。这行代码里读取了两个配置文件:config.yml和config-dev.yml,两个文件中的配置有重合之处(不详细贴配置信息了,可参考后文示例代码),但是最终输出信息以后者中的配置为准。

适应性选择配置

虽然这个写在最后,不过应该是最好解决的事情了,通过环境变量、命令参数都能解决。为此写段演示代码意义好像也不大,就这样吧。

文中示例代码见git:yaml-reader

End!!

Golang Json 处理

这次记录下golang json处理的几个场景:

  1. 不输出空值
  2. 输出时使用别名
  3. 不输出指定字段

不输出空值

不希望输出空值,可以在注解中使用omitempty,示例代码如:

输出时使用别名

依然使用注解,示例:

不输出指定字段

不输出指定字段有两种方式:

  1. 变量首字母小写,匿名变量
  2. 使用注解,如下:

就是这些了。 示例代码 Json Ignore

End!

springboot入门14 – Kafka应用

简述

这几天优化了一下之前写的一个springboot kafka组件。比较起原生的spring-kafka来,我希望能够简化kafka的使用,可以更聚焦于具体的消息处理逻辑。

接下来的内容是这个组件的用法。

使用方法

添加依赖

这个组件已经提交到了maven中央仓库,可以直接通过依赖的形式引入:

0.2.2是这两天刚发布的一个版本。

消费者Processor

kafka-spring-boot-starter这个组件已经完成了kafka消费者的主要功能。

对于开发者来说,可以不必关注KafkaConsumer的创建,只需要实现Processor接口并注入到容器中即可。

下面是一个简单的示例:

如示例中,通过@Component注解完成了Processor实现类的实例的注入,并为注入的Bean提供了一个名称:zhyyy。记住这个名称,在之后的配置文件中会用到。

使用生产者

kafka-spring-boot-starter会根据配置主动创建KafkaProducer。开发使用时可以直接从容器中获取ProducerTemplate实例来发送消息:

如果写入kafka的消息的key和value的序列化方案采用的都是默认的字符串(反)序列化方案(StringDeserializerStringSerializer),可以使用StringProducerTemplate实例:

发送消息时酌情调用不同的send()方法:

配置

下面是一个最简单的配置:

如上配置中:

  • test-group00既是配置项的ID,也是消费组ID
  • bootstrap-servers我想不需要多做解释。
  • topics对应的是一个数组结构,也可以写作[test-topic1][test-topic1,test-topic2],即支持同一个kafka集群上多个类似topic的统一处理
  • consumer是消费者相关配置,processor对应的是Processor实现类的Bean名称,count标识的是应用内消费线程的数量

默认的序列化方案采用的是字符串序列化方案。

虽然在配置中没有体现,但kafka-spring-boot-starter组件会基于已有的信息创建KafkaProducer,使用时可以通过ProducerTemplate执行消息发送。

比较完整的配置是这样子的:

其中common模块下是一些通用的配置,config模块下则是一或多组具体的配置(这里是两组)。common下的配置会被config下的配置覆盖。

此外还独立出来了一些常用的配置项,如autoOffsetResetkeyDeserializer等,以便在使用时进行配置。

其他

示例应用在 github / spring-boot-kafka

kafka-spring-boot-starter这个组件的源码也在 github 。如果有定制化的需求可以据此进行调整。

End!

springboot入门13 – 多CacheManager应用

概述

之前有写过springboot缓存应用的说明(《springboot入门01 – 缓存的使用》)。不过实际的场景有时候会比较复杂一些,比如:需要同时使用redis和caffine来做多级缓存,或者需要在通用配置外应用一些个性化的配置。使用多个CacheManager来分别管理不同的缓存是应对这种问题的一个常规方案。

接下来介绍下如何实现多CacheManager应用。

CacheManager应用

这里会通过一个具体的案例来进行演示。需求大致是这样的:在应用中的大部分场景都使通用的缓存配置,但是部分特殊场景需要做个性化的配置。在接下来演示中我们主要会用到Caffeine缓存。

配置

先修改下配置。这次如果继续使用SpringBoot的自启动CacheManager会有一些不太好控制的地方,因此不宜再使用默认的缓存配置,需要做些独立配置:

其中 caching.spec 表示通用的缓存配置, caching.special则表示一组需要做个性化配置的特例。

通用配置是一行字符串,读取的时候可以直接使用@Value注解,个性化配置则是一个Map结构,读取的时候需要用到@ConfigurationProperties注解,大致如下:

(关于配置文件读取可以参考之前的一篇旧文:《springboot入门07 – 配置文件详解》)

创建CacheManager

接下来就是根据配置文件创建CacheManager了。

创建通用CacheManager直接使用CaffeineCacheManager就可以了:

注意@Primary注解,这个注解表示没有特意注明时,优先选择这个CacheManager

管理个例的CacheManager略有些麻烦,这里使用了SimpleCacheManager,代码如下:

在使用这个CacheManager时还需要记得下在@CacheConfig@Cacheable注解中注明对应的qualifier:

至此,多缓存配置已经是没有问题了。详细代码可以参考Git: zhyea / multi-cache

这里区分通用CacheManager与个例CacheManager主要依赖@Primary注解实现。接下来会介绍一些其他的方案。

继承CachingConfigurerSupport

继承抽象类CachingConfigurerSupport后,可以通过实现(重写)cacheManager()方法指明默认的CacheManager。嗯,也就是说,节省了一个@Bean和一个@Primary注解。代码如下:

管理个例的CacheManager还是需要@Bean注解并设置Qualifier的。

实现CacheResolver

这个方案,怎么说呢,应该是可操作性最强大的。如果场景再复杂些,完全可以考虑用这个方案来处理。但是就我们当前这个case来说,用CacheResolver来实现应该是最繁琐的了。太繁琐了,懒得写了。

姑且写一个简单的例子来演示下CacheResolver是怎么发挥作用的吧。

CacheResolver的实现类如下:

在代码中可以看到,是通过resolveCaches()方法决定了提供哪些缓存。

使用CacheResolver后就有机会可以考虑不注入CacheManager的实例到容器中了,因为CacheResolver会管理会用到的CacheManager的实例。 不过在应用缓存注解的情况下,要记得指定使用哪个CacheResolver,像这样:

就这样了。示例代码都放在了这里:zhyea / multi-cache

参考文档

springboot入门12 – SpringBoot MyBatis读写分离

概述

随业务量增长,数据库读写分离是迟早要面临的问题。另外,公司在上规模后一般也会要求统一采用主从分布式数据库。

我习惯的处理方案是在应用层进行隔离:即将以写为主的业务放在一个应用上,以读为主的业务放在其他应用上。这应该算是最简单粗暴的解决方案了,却也能帮我应对90%需要读写分离的场景。不过总还有10%的特殊场景需要思考下怎样在应用内实现读写分离。

在应用内做读写分离大体上需要考虑三件事情:

  1. 多数据源实现
  2. 读写请求识别
  3. 读写请求分流

其中后两点是执行读写分离的关键。

接下来详细介绍下怎么在Springboot+MyBatis的应用中实现读写分离。这里会用到H2数据库和dbcp2数据库连接池。在测试中不会真的创建一个数据库集群,我们只需要能够验证写入和读取是访问的两个不同的数据库即可。

1. 多数据源实现

之前在《SpringBoot自定义数据源及多数据源配置》这篇文里我有介绍过怎样做多数据源实现。这次的做法也差不多。

下面是在配置文件中做的多数据源配置:

这里配置了两个H2的内存数据库,我们权且当它们是一个集群吧。

然后是在一个配置类DsConfig中读取配置:

和之前那篇文章《SpringBoot自定义数据源及多数据源配置》略有不同,这次是用DataSourceProperties来表示读取的配置信息。

注意不要忽略了@Primary注解,不然会报错。

可以这样使用DataSourceProperties创建DataSource的实例:

这里只是想试试这种方案。按照老路子创建DataSource实例也是OK的,并且还会更简洁:

至此多数据源配置已经完成。

2. 读写请求识别

也看过其他人的读写分离方案,其中读写请求识别这一层多是通过自定义注解+AOP来实现的。这种方案当然没问题,但是稍嫌有些繁琐,如果忘掉了添加注解就会导致意外。我更想要的是一种‘润物细无声’的实现。

之前做过一个为写入数据库的实例赋默认值的方案:《MyBatis写入时null问题统一处理方案》。这个方案的思路是通过自定义实现MyBatis拦截器来拦截数据库请求并补上未赋值的数据。稍稍变通下,就可以改为拦截并识别查询语句:

查询请求主要是由Executor类的query方法实现的,所以只要针对其进行拦截并执行判断即可。

因为判断读写请求和执行读写分流是在两个环节执行,我们需要找个地方将判断结果存储起来,并且保证线程安全,很自然可以想到使用ThreadLocal执行存储。DsContextHolder就是基于ThreadLocal执行的存储。看下实现:

接下来就可以使用DsContextHolder中存储的信息来执行分流了。

3. 读写请求分流

读写请求分流这一层主要依赖了AbstractRoutingDataSource这个类。核心是下面这个方法:

看名字也能知道,determineTargetDataSource决定了为之后的请求提供哪个数据源。根据代码可以看出来,我们至少需要做两件事:

  1. resolvedDataSources赋值,即设置相关数据源
  2. 实现分流方法determineCurrentLookupKey()

具体如何继承AbstractRoutingDataSource类并实现路由方案可以参考如下代码:

afterPropertiesSet()方法中完成了自定义数据源的创建和设置,并且还将dsWrite设置为了默认数据源。

方法determineCurrentLookupKey()基于DsContextHolder中存储的内容提供了分流的关键字。

大体上就是这样了。具体实现代码已经上传到了GitHub : zhyea/database-wr

End!!

参考文档

  1. mybatis读写分离

Excel导出超过65535行报异常

工程中在使用POI导出Excel数据,某次因为导出的数据量比较大就报了下面的错误:

查了下资料,了解到问题在于使用了HSSFWorkbookHSSFWorkbook用来操作Excel2003以前(包括2003)的版本,限制了每个Sheet导出的行数至多为65535行。

要修复这个问题可以考虑使用XSSFWorkbook或者SXSSFWorkbook。如果要导出的数据量特别大,建议使用后者。因为XSSFWorkbook会将全部要写入Excel中的数据保存在内存中,数据量太大的话就会导致内存溢出错误。

SXSSFWorkbook是在POI3.8版本时出现的,对XSSFWorkbook做了一些优化,通过采用将一定数量的数据写入硬盘临时文件的机制减轻了内存的压力。

参考文档

HSSF XSSF SXSSF

SpringBoot读写xml上传到S3

最近的工作涉及到了生成xml文件并上传到AWS存储服务S3这样的处理。期间遇到了两个问题,简单记录下:

  1. springboot读取xml模板异常
  2. 将生成的xml上传到S3的问题

springboot的版本是2.1.9.RELEASE,读写xml文件使用的是Dom4J,版本是2.1.3。逐个说明下遇到的这几个问题。

1.springboot读取xml模板异常

现阶段是将xml模板文件存储在springboot项目的resource目录下的。具体路径为

最初是通过类加载器获取文件路径后再尝试读取模板文件的:

通过类加器获取到的文件路径是:

不过我们都知道,springboot是将整个工程包括配置文件打成一个jar包后再直接运行。这样想在linux的服务器上通过文件路径找文件是注定找不到的。

后来改成直接通过SpringBoot提供的ClassResource类来获取resource路径下的配置文件:

这里直接使用InputStream读取的模板文件。注意不要再尝试通过调用ClassResource实例的getFile()方法来获取文件,不然会遇到和之前同样的问题。

额,期间还发生了无法将模板文件打进springboot项目运行时的jar文件这样的问题。因为是将模板文件存储在了resources的子目录下,需要调整下maven打包的配置:

下面这几行如果没有的话需要加上,不然会读取不到子目录中的配置文件:

2.将生成的xml上传到S3

AWS提供的最便捷的上传文件接口是这个:

这个接口通过File实例来执行上传。所以我一开始的想法是先生成一个临时文件保存在服务器本地,读取本地临时文件为File执行上传,最后再删掉本地的临时文件。这个思路是没问题的,在本地执行也OK。但是在生产环境,由于权限相关的问题,生成临时文件失败了。

不想再去折腾权限相关的事情,所以将出路寄托在了AWS提供的另一个接口上:

也就是说考虑将xml文件内容输出到InputStream,然后再将InputStream上传到S3。一切都在内存里执行,不依赖外部文件系统也就不会有文件权限的问题。

这个方案的问题在于ObjectMetaData这个类有点儿黑箱的意思。该怎么设置需要进行一些摸索。看了一遍这个类的接口文档,需要调用的也就这两个set方法:

其中后者(文件长度)是AWS建议设置的,不设置会在处理的时候给出WARN。根据方法文档也可以看到,如果不设置,在上传的时候就会在内存中缓存整个信息流来计算文件长度。

至于前者是上传到S3文件的缓存过期时间,酌情设置即可。

另一个需要解决的问题就是怎么将Dom4j生成的Document输出再读取到InputStream中。这里用到了XmlWritter类,具体实现如下:

验证了一下,这个方法是可行的。修改后生产环境没有再报错。

向AWS S3存储服务上传文件的实现代码在这篇文章里:Java实现上传文件到AWS S3

End!

Java SpringBoot实现上传文件到AWS S3

简单记录一下在Springboot中上传文件到AWS S3存储服务的代码。

application.xml中添加aws相关配置:

新建一个AwsS3Componment类来执行上传文件操作:

因为使用的服务有设置endpoint,所以这里需要使用下面这一行完成endpoint的设置:

如果不设置endpoint就会收到下面这样的报错:

异常信息中提示了AccessKey无效——虽然我的AccessKey是有效的。

endpoint的这行配置中还设置了region信息。如果不需要设置endpoint,就得补上region的配置:

下面是执行上传的代码:

这里是通过File实例执行的上传。有时候会需要直接通过文件流执行上传,此时可以使用下面的代码:

注意这里的setContentLength()最好配置一下。不设置会在处理的时候给出WARN。根据方法文档也可以看到,如果不设置,在上传的时候就会先在内存中缓存整个信息流来计算文件长度。

大体上就是这样了。

End!

Jackson处理json中的反斜杠

在使用jackson处理json时遇到了反斜杠相关的问题。比如我们从数据库或文件中读到了这样的一个json字符串:

可以看到在World前面有一个反斜杠。因为这个反斜杠,JetBrains IDEA会提示这是一个错误的字符串,使用jakcson处理这个json字符串时也会报错,错误信息如下:

针对这种问题,Jackson已经预设了配置项: ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER

在Jackson早期的版本中,可以这样配置:

但是在最近的版本中,这个配置项被移到了JsonReadFeature类中。对此我是很惆怅的:因为没办法简简单单直接使用configure方法进行配置了。

Em,在最近的Jackson的版本中需要这样配置:

如果已经有一个ObjectMapper实例了,还可以考虑创建一个ObjectReader实例读取数据:

大体上就是这样了。

参考文档

How to use JsonReadFeature

End!

springboot入门11 – MyBatis写入时null问题统一处理方案

如果MySQL表的一些字段被设置为不允许为空,使用MyBatis写入的数据中相应字段的值是null就有可能会报类似下面的错误:

对于这个问题,我通常的做法是为对应字段的null值直接赋值一个空字符串(或其它默认值)。但是这次,不知道为什么那么多不必要的字段被设置为了不能为空:七八个字段,要判断是否为空,还要一个个赋默认值 —— 想想就头疼。一定要想办法优化掉这种啰嗦且难看的东西。

参考分页插件 PageHelper 的实现后找到了思路:添加一个拦截器,获取参数实例,为实例的值为null的Field设置一个默认值。

拦截器的实现如下:

在这个拦截器中通过反射的方式为参数实例值为null的字段完成了赋默认值。这里只为几个我常用的类型(字符串、整型和日期类型)设置了默认值,如果还会用到其他值请自行修改setDefaultValue方法。

代码中有一个重要的点就是拦截器类上的注解@Signature

这个注解指明了具体要拦截的类和方法。在这里,数据库操作主要是通过Executor类完成;并且不论是新增或是更新都是通过update方法实现的,args属性则明确了方法参数只有一个Object类型的值。

然后是让这个拦截器生效。代码如下:

这里通过@Configuration完成的自定义拦截器的注入。

配置完成后,再次执行写入和更新的测试案例,一次通过,搞定!

示例代码:zhyea / spring-boot-database-pad