【基础系列】自定义属性配置绑定极简实现姿势介绍
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
9007199254740992
大于9007199254740992
的可能会丢失精度因此对于java后端返回的一个大整数,如基于前面说到的雪花算法生成的id,前端js接收处理时,就可能出现精度问题
接下来我们以Thymeleaf模板渲染引擎,来介绍一下对于大整数的精度丢失问题的几种解决方案
首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
添加web支持,用于配置刷新演示
1 | <dependencies> |
接下来配置一下db的相关配置 application.yml
1 | server: |
首先我们借助Thymeleaf创建一个简单的页面,用于返回演示长整型的使用
模板网页如下
1 |
|
我们直接借助前面实现的Snowflake来生成长整数,写一个对应的接口
1 |
|
直接访问,表现如下
从截图可以看出,再html标签中,直接使用${hu}
获取长整型时,显示正常;
但是js中,获取的长整型,则出现了精度丢失问题
如控制台中打印的 console.log("hu = ", hu);
最后的几位变成了0,与实际不符
对于长整型导致的精度问题,最容易想到也是最推荐的解决方案,即对于long类型的参数,改为String方式进行返回,让前端以String的方式进行处理,从而解决精度丢失问题
方案1:修改后端的返回,将长整形改String
如将上面的流程如下修改:
1 | "show") ( |
方案2:前端js使用String方式接收长整形
1 | <script th:inline="javascript"> |
具体的效果就不再演示,有兴趣的小伙伴可以自己体验一下;这种方式虽然简单有效,但是对现有的项目改造还是挺大的,且很容易有遗漏;自然的,我们就会思考一下,是否有统一的处理方式来解决这种问题
作为后端,前端的使用姿势我们无法控制;为了整个程序的准确性,后端直接返回String格式通常是首选的方案;对于现下主流的前后端分离方案,后端一般是返回json格式的数据,所以要想实现统一的格式转换,自然会想到对序列化做文章
比如SpringBoot默认的jackson序列化框架,直接让其实现对长整型转String的转换
先实现一个工具类,来实现上面的诉求,支持long/bigint/bigdecimal转string
1 | public class JacksonUtil { |
其次就是注册一个支持长整型转String的序列化转换类HttpMessageConverter
1 | 4j |
接下来我们对比一下,上面注册前后,访问 ‘http://localhost:8080/id2' 返回的数据格式
基于上面的输出结果,可以看到我们的目标已经实现,返回的长整型会自动转换为字符串;这样前端使用时,就不会出现精度丢失问题了(除非前端又将字符串转number)
上面这个是后端直接返回Json对象数据;这种解决方案适用于 Thymeleaf
模板渲染引擎么?
http://localhost:8080/show
看一下控制台输出why?
Thymeleaf模板的参数传递,并不是通过
HttpMessageConverter
来实现的,数据转换的实现主要是靠IStandardJavaScriptSerializer
既然直接返回json数据可以通过修改序列化的转换方式来实现,那么Thymeleaf按照这个思路,应该也是可行的
直接通过debug,我们可以知道Thymeleaf默认使用的是JacksonStandardJavaScriptSerializer
来对js传递的对象进行序列化
从JacksonStandardJavaScriptSerializer
的实现来看,比较遗憾的是它并没有支持长整型转字符串,也没有预留给我们进行注册Module
的口子
因此一个粗暴的解决方案就是反射拿到它,然后进行主动注册
1 | import com.fasterxml.jackson.databind.ObjectMapper; |
上面配置完毕之后,正常我们再js中获取到的长整型就会变成字符串,不会再出现精度丢失问题了;直接再次验证一下,正常输出应该如下:
使用反射的方式虽然可以解决我们的诉求,但是不太优雅,既然官方定义了接口,我们完全可以注册自定义实现,来解决这个问题
1 | /** |
然后再将我们自定义的是转换类注册到TemplateEngine
1 |
|
本文的内容相对较多,但是核心的问题解决思路只有一个:
对于长整型的精度问题,解决方案就是将长整型转换为字符串
对应的解决方案有下面几种
HttpMessageConverter
做统一的长整型格式化转换IStandardJavaScriptSerializer
支持长整型的格式转换最后再抛出一个问题,上面给出了Thymeleaf的长整形转换,但是如果我用的是Freemaker渲染引擎, 序列化工具使用的是gson, fastjson,那应该怎么处理呢?
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
雪花算法主要是为了解决全局唯一id,那么什么是全局唯一id呢?它应该满足什么属性呢
基本属性:
雪花算法可以说是业界内生成全局唯一id的经典算法,其基本原理也比较简单
Snowflake 以 64 bit 来存储组成 ID 的4 个部分:
从上面的结构设计来看,雪花算法的实现可以说比较清晰了,我们重点看一下它的缺陷
目前雪花算法的实现方式较多,通常也不需要我们进行额外开发,如直接Hutool的Snowflake
看下它的核心实现
1 | public class Snowflake implements Serializable { |
关键实现在 nextId()
方法内,做了两个保护性兼容
接下来看一下实际的使用
1 | private static final Date EPOC = new Date(2023, 1, 1); |
输出如下:
1 | 1717380884565065728 |
在某些时候我们对雪花算法的实现有一些特殊的定制化场景,比如希望生成的id能一些更具有标识性,如以商城领域的订单数据模型为例
再比如对订单的长度希望做一些限制,19位太多了,我希望16、7位的长度
再比如我希望调整workerId 与 datacenter之间的分配比例
基于以上等等原因,当我们面对需要修改雪花算法逻辑时,再知晓算法原理的基础上,完全可以自己手撸
1 | 4j |
注意上面的实现,相比较于前面HuTool的实现,有几个变更
接下来再看下实际的使用输出
1 | public static void main(String[] args) throws InterruptedException { |
输出如下
1 | 23299055409901569 |
雪花算法本身的实现并不复杂,但是它的设计理念非常有意思;业界内也有不少基于雪花算法的变种实现,主要是为了解决时钟不一致及时钟回拨问题,如百度UIDGenerator
,美团的Leaf-Snowflake
方案
雪花算法其实是依赖于时间的一致性的,如果时间回拨,就可能有问题,其次机器数与自增序列虽然官方推荐是10位与12位,但正如没有万能的解决方案,只有最合适的解决方案,我们完全可以根据自己的实际诉求,对64个字节,进行灵活的分配
再实际使用雪花算法时,有几个注意事项
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
本文将介绍三种sql日志打印的方式:
首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
添加web支持,用于配置刷新演示
1 | <dependencies> |
接下来配置一下db的相关配置 application.yml
1 | spring: |
关于上面配置的一些细节,后面进行细说
我们创建一个用于测试的数据库
1 | drop table `money` if exists; |
对应的myabtis-config.xml,配置我们自定义的sql日志输出拦截器
1 |
|
我们先看一下mybatis的默认日志输出方案,首先写一个money
数据库的db操作mapper
1 |
|
接下来重点看一下,如需开启myabtis默认的sql日志输出,应该如何配置
1 | mybatis: |
重点看上面的 mybatis.configuration.log-prefix
与 myabtis.configuration.log-impl
这里制定了日志输出的方式
但是请注意,通常我们的日志是基于logback/slf4j
来输出,默认的mybati的sql日志输出是debug级别,所以要看到输出的sql日志,还需要配置一下日志输出级别(本项目的实例中是直接控制台输出,因此不配置下面的也没有问题)
1 | # 日志打印级别 |
然后写个demo验证一下
1 | 4j |
从上图可以看出,myabtis将具体的sql执行,返回的行数等信息进行了返回,但是这个sql,并不是一个可以直接执行的,还需要我们自己来拼装一下,为了解决这个问题,可以通过 https://book.hhui.top/sql.html 来进行sql的自动拼接
除了mybatis的默认日志之外,对于使用druid数据源的场景,也可以直接借助druid来打印执行日志
核心的配置如下
1 | spring: |
示例如下
1 | Map map = jdbcTemplate.queryForMap("select * from money where id = ?", po.getId()); |
druid的默认输出日志中,并没有将请求参数打印出来,其效果相比较于mybatis而言,信息更少一些
默认的输出方案虽好,但是总有一些缺陷,如果有一些自定义的诉求,如日志输出的脱敏,不妨考虑下接下来的基于mybatis的拦截器的实现方案
如下面是一个自定义的日志输出拦截器, 相关知识点较多,有兴趣的小伙伴,推荐参考下文
1 | 4j |
然后将第一种测试用例再跑一下,实际输出如下
本文主要介绍了三种常见的sql日志输出方案,原则上推荐通过自定义的插件方式来实现更符合业务需求的sql日志打印;但是,掌握了默认的myabtis日志输出方案之后,我们就可以借助配置中心,通过动态添加/修改 logging.level.com.git.hui.boot.db.mapper.*
来动态设置日志输出级别,再线上问题排查、尤其时场景可以复现的场景时,会有奇效哦
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
本文将主要介绍jwt的相关知识点,以及如何基于jwt来实现一个简单的用户鉴权方案
jwt,全称 json web token, JSON Web 令牌是一种开放的行业标准 RFC 7519 方法,用于在两方之间安全地表示声明。
详情可以参考: hhttps://jwt.io/introduction
JSON Web Token由三部分组成,它们之间用圆点.
进行分割, 一个标准的JWT形如 xxx.yyy.zzz
即第一部分,由两部分组成:token的类型(JWT
)和算法名称(比如:HMAC
SHA256
或者RSA
等等)。
一个具体实例如
1 | { |
然后,用Base64对这个JSON编码就得到JWT的第一部分
第二部分具体的实体,可以写入自定义的数据信息,有三种类型
Registered claims
: 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer 签发者), exp (expiration time 有效期), sub (subject), aud (audience)等。Public claims
: 可以随意定义。Private claims
: 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明如一个具体实例
1 | { |
对payload进行Base64编码就得到JWT的第二部分
为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可。
如 HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
下面给出一个基于 java-jwt
生成的具体实例
1 | public static void main(String[] args) { |
接下来我们基于jwt方案实现一个用户鉴权的示例demo
首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
添加web支持,用于配置刷新演示
1 | <dependencies> |
我们采用thymeleaf来进行前端页面的渲染,添加一些相关的配置 application.yml
1 | server: |
一个简单的基于jwt的身份验证方案如下图
基本流程分三步:
用户登录成功之后,后端将生成的jwt返回给前端,然后前端将其保存在本地缓存;
之后前端与后端的交互时,都将jwt放在请求头中,我们这里借助Http的身份认证的请求头Authorization
后端接收到用户的请求,从请求头中获取jwt,然后进行校验,通过之后,才响应相关的接口;否则表示未登录
基于上面的流程,我们可以实现一个非常简单的登录认证演示工程
首先在内存中,维护几个简单用户名/密码信息,用于模拟用户名+密码的校验
1 |
|
然后提供登录接口
1 | "/login") (path = |
上面的接口实现,接收两个请求参数: 用户名 + 密码
当用户身份校验通过之后,将生成一个jwt,这里直接使用开源项目java-jwt
来生成(当然有兴趣的小伙伴也可以自己来实现)
需要注意的一点是,我们在上面的实现中,除了直接返回jwt之外,也将这个jwt写在cookie中,这种将jwt写入cookie的方案,主要的好处就是前端不需要针对jwt进行特殊处理
当然对应的缺点也和直接使用session的鉴权方式一样,存在csrf风险,以及对于跨资源共享时的资源共享问题(CORS)
本项目的实际演示中,采用前端存储返回的jwt,然后通过请求头方式来传递jwt
上面登录完成之后,再提供一个简单的要求登录之后才能查看的查询接口
1 | "query") ( |
最后再写一个前端页面来完成整个测试
1 | "/", "", "/index"}) (path = { |
对应的前端页面如下:
1 |
|
基于上面的实现,接下来我们看一下具体表现情况
从上面的两张图也可以看出,登录成功之后,jwt写入到本地的session storage中,再后续的请求中,若请求头Authroization
中携带了jwt信息,则后端可以进行正常校验
有兴趣的小伙伴可以尝试修改一下本地存储中的jwt值,看一下非法或者过期的jwt会怎么表现
本文主要介绍了jwt的基本知识点,并给出了一个基于jwt的使用实例,下面针对jwt和session做一个简单的对比
jwt | session |
---|---|
前端存储,通用的校验规则,后端再获取jwt时校验是否有效 | 前端存索引,后端判断session是否有效 |
验签,不可篡改 | 无签名保障,安全性由后端保障 |
可存储非敏感信息,如用户名,头像等 | 一般不存储业务信息 |
jwt生成时,指定了有效期,本身不支持续期以及提前失效 | 后端控制有效期,可提前失效或者自动续期 |
通常以请求头方式传递 | 通常以cookie方式传递 |
可预发csrf攻击 | session-cookie方式存在csrf风险 |
关于上面的两个风险,给一个简单的扩展说明
csrf攻击
如再我自己的网站页面上,添加下面内容
1 | <img src="https://paicoding.com/logout" style="display:none;"/> |
然后当你访问我的网站时,结果发现你在技术派上的登录用户被注销了!!!
使用jwt预防csrf攻击的主要原理就是jwt是通过请求头,由js主动塞进去传递给后端的,而非cookie的方式,从而避免csrf漏洞攻击
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
@Value
或者 @ConfigurationProperties
来引用那么配置信息只能放在这些配置文件么? 能否从db/redis中获取配置信息呢? 又或者借助http/rpc从其他的应用中获取配置信息呢?
答案当然是可以,比如我们熟悉的配置中心(apollo, nacos, SpringCloudConfig)
接下来我们将介绍一个不借助配置中心,也可以实现自定义配置信息加载的方式,并且支持配置的动态刷新
首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
添加web支持,用于配置刷新演示
1 | <dependencies> |
我们使用默认的配置进行测试,因此启动入口也可以使用最基础的
1 |
|
我们的目标是实现一个自定义的配置信息加载,并支持配置与Spring bean对象的绑定,同时我们还需要支持这个配置的动态刷新
基于上面这个目标,要想实现则需要几个知识储备:
结合上面的知识点,我们主要需要实现的有三步:
为了简化自定义的配置使用,我们这里直接使用一个内存缓存来模拟自定义的配置源
1 |
|
注意上面的实现,这里是自定义的配置源 propertySources 中包含了environment的配置信息;如果希望将自定义的配置信息源注入到environment,可以如下实现
1 | MapPropertySource propertySource = new MapPropertySource("selfSource", SelfConfigContext.getInstance().getCache()); |
接下来我们自定义一个注解@ConfDot
, 凡是带有这个注解的bean的成员变量,从上面的属性源中进行初始化
这个注解可以完全按照@ConfigurationProperties
的来设计(实际上我们也可以直接使用@ConfigurationProperties
注解,这样适用范围更广了)
1 | ({ElementType.TYPE, ElementType.METHOD}) |
然后借助Spring来扫描带有特定注解的bean,就可以很简单了
1 | applicationContext.getBeansWithAnnotation(ConfDot.class).values().forEach(bean -> { |
上面两部完成之后,接下来就需要我们将配置与bean进行绑定,这里就主要使用Binder来实现我们的预期功能了
实现一个自定义的绑定工具类
1 | public class SelfConfigBinder { |
上面的实现虽然多,但是核心其实比较简单:
this.binder = new Binder(getConfigurationPropertySources(), getPropertySourcesPlaceholdersResolver(), getConversionService(), getPropertyEditorInitializer());
1 | public <T> void bind(Bindable<T> bindable) { |
上面的三步实现,基本上已经将整个功能给实现了,其中SelfConfigBinder
提供了完成的代码实现,接下来我们再将第一步与第三步的整合,来看一下完整的实现,并且提供一个配置刷新的支持
1 |
|
接下来就是验证一下上面的设计,首先再配置文件中,添加几个默认的信息
1 | config: |
绑定配置的bean对象
1 |
|
上面这个MyConfig中的 user, pwd 从前面的配置文件中获取,然后type则此自定义的配置信息configCache
中获取,应该是12,接下来我们首先一个访问与刷新的接口
1 | 4j |
实际执行测试如下图
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
@Value
, @ConfigurationProperties
来实现属性配置与Java POJO对象、Bean的成员变量的绑定,那如果出现一个某些场景,需要我们手动的、通过编程式的方式,将属性配置与给定的pojo对象进行绑定,我们又应该怎么实现呢?首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
我们使用默认的配置进行测试,因此启动入口也可以使用最基础的
1 |
|
本文的目的主要是给大家介绍编程式的属性绑定,当然除了我们最熟悉的直接写代码,从Environment
中获取配置之外,还可以使用Binder来更方便的实现属性绑定
因此我们首先来了解一下这个不太常出现在CURD的日常工作中的Binder类:
1 | // 获取binder实例 |
两种常见的使用姿势:
接下来我们看几个常见的使用姿势
直接将配置绑定到我们自定义的属性配置类上,也就是我们最常见的、可直接利用@ConfigurationProperties
来实现的使用方式
我们在配置文件中,添加一个基础的配置
1 | demo: |
接下来定义一个对应的属性配置类Mail
1 |
|
然后我们的使用姿势,将如下
1 |
|
在上面的基础使用姿势之上,我们再加两个使用姿势
微调一下上面的bindInfo()方法
1 | public void bindInfo() { |
执行之后,输出如下
从上面的输出可以看出,对于
demo.mail2
的都没有)demo.mail.user
这个配置不存在时(配置中的是username),此时bind/bindOrCrate 返回的对象中,相关的属性是null (主意这种场景 bind 方法调用不会抛移异常,有兴趣的小伙伴可以实际验证一下)在实际的应用场景中,配置为数组的可能性也很高,比如我有一个代理库,对应的相关配置如下
1 | demo: |
此时我们的实际使用姿势可以如下
1 |
|
对应的手动绑定方式
1 | // 将配置绑定到list |
输出结果如下
1 | list config: [BindHelper.Proxy(ip=127.0.0.1, port=1080), BindHelper.Proxy(ip=localhost, port=1800)] |
将属性配置绑定到一个Map的场景也不算少见,如之前写过的多数据源自主切换的实现方式中,就有这么个场景
我们写一个简单的配置模拟上面的场景
1 | demo: |
在上面的配置中,master/slave 为数据源名称,在下面的配置则为数据源配置信息,结构都一致;基于此,我们需要声明的配置类实际为
1 |
|
配置绑定的实现也很简单,与上面List的类似
1 | Map<String, DsConfig> dsMap = binder.bind("demo.dynamic", Bindable.mapOf(String.class, DsConfig.class)).get(); |
执行之后的输出结果如下
1 | Map Config: {master=BindHelper.DsConfig(user=main, password=m1), slave=BindHelper.DsConfig(user=slave, password=s1)} |
上面介绍的姿势都是直接将配置绑定到对应的java对象上,那么我们是否会存在需要对配置属性进行特殊处理的场景呢?
这种场景当然也不算少见,如驼峰与下划线的互转,如密码之类的配置文件中属于加密填写,应用加载时需要解密之后使用等
对于这种场景,我们也给出一个简单的实例,在配置文件中,添加一个base64加密的数据
1 | demo: |
对应的解析方式
1 | // 对配置进行解析 |
执行之后,实际输出结果如下:
1 | 解码之后的数据是: 一灰灰blog |
除了上面介绍到的属性绑定姿势之外,Binder还非常贴心的给大家提供了过程回调,给你提供更灵活的控制方式
1 | // 注册绑定过程回调 |
同样是实现配置解密,如上面的方式也是可行的,对应的输出如
1 | 开始绑定: demo.enc.pwd |
本文的知识点比较简单,属于看过就会的范畴,但是它的实际应用场景可以说非常多;特别是当我们在某些场景下,直接使用SpringBoot的属性配置绑定不太好实现时,如动态数据源、配置的回调处理等,不妨考虑借助Binder来实现编程式的配置绑定加载
其次本文只介绍了Binder类的使用姿势,有好气的小伙伴,自然会想了解它的具体实现姿势,它是怎么实现配置属性与java实体类进行绑定的呢? 类型转换如何支持的呢? 如果让我们自己来实现配置绑定,可以怎么支持呢?
不妨再进一步,让我们实现一个自定义的配置加载、解析、绑定并注入到Spring容器的解决方案,可以怎么整?
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1 | <dependencies> |
我们使用默认的配置进行测试,因此启动入口也可以使用最基础的
1 |
|
我们主要根据用户的session来创建与销毁来判断是否有新的用户访问站点、以及长时间没有访问之后认为已经离线,为了简化这个注销的模拟过程,我们将session的生命周期设置短一点
1 | server: |
接下来我们看一下具体的实现思路:
一个简单基础的计数服务,借助 AtomicInteger
来实现计数统计(为啥不直接是int ?)
1 |
|
自定义一个Session的监听器,监听HttpSession的相关操作
1 |
|
最后再设计一个登录、登出、查询实时在线人数的统计接口
1 |
|
接下来验证一下,实时在线人数统计情况
上面虽然是实现了实时在线人数统计,但是存在一个非常明显的短板问题,那就是只适用于单机的场景,如果后台有多个服务部署,那应该怎么处理呢?
基于此,自然而然想到的就是分布式session 结合 redis 计数来实现,但是这个思路可行么? 分布式session失效会抛出一个事件么?或许通过监听redis的key失效能处理,但是整体来看,还是有些麻烦,有没有更简单实用的场景呢
且待下文详解
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1 | <dependencies> |
我们使用默认的配置进行测试,因此启动入口也可以使用最基础的
1 |
|
接下来我们再看一下如何在SpringBoot项目中是session/cookie
首先我们设计一个登录的接口,用来模拟真实场景下的登录,注意下面的实现
1 |
|
在上面的实现中,方法中定义了一个HttpSession
的参数类型,具体的实现中,就是表示写入sesion的操作
当session写入完毕之后,在这个会话结束之前,后续的所有请求都可以直接获取到对应的session
下面给出两种常见的session获取方式
1 | "time") ( |
接下来我们来模拟验证一下
从上面的演示图中,也可以看出,在登录之后,访问上面的接口,可以直接拿到session中存储的用户名;
且不同用户登录(不同的浏览器),他们的session不会出现串掉的情况
有登陆当然就有登出,如下
1 | "logout") (path = |
SpringBoot提供了一套非常简单的session机制,那么它又是怎么工作的呢? 特别是它是怎么识别用户身份的呢? session又是存在什么地方的呢?
session:再浏览器窗口打开期间,这个会话一直有效,即先访问login,然后再访问time,可以直接拿到name, 若再此过程中,再次访问了login更新了name,那么访问time获取到的也是新的name
当浏览器关闭之后,重新再访问 time 接口,则此时将拿不到 name
核心工作原理:
从上面的描述中,就可以看出几个关键点:
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
接下来我们来看一下两种不同的方式,来实现上面的诉求
首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1 | <dependencies> |
与前面不同,我们不需要在配置文件中指定缓存类型以及caffeine的相关条件参数,直接放在配置类中
1 |
|
注意上面的 cacheList,其中传入的就是Cache
对象,每个Cache对象就可以理解为一个缓存实例,重点注意构造参数中的第一个customCache
,这个就是后面缓存具体使用时,注解中的cacheNames
属性
1 |
|
重点注意一下上面的@CacheConfig
,它定义了这个类中的的缓存,都使用 customCacheManager
缓存管理器,且具体的缓存为定义的customCache
(改成其他的会报错)
从上面的配置声明,也可以看出,当我们希望使用多个缓存时,可以直接如下面这种方式进行扩展即可
1 |
|
除了上面这种方式之外,我们当然也可以再额外定义一个CacheManager,如下
1 | "otherCacheManager") ( |
使用上面这种方式,cacheName可以不需要指定,具体使用如下
1 | /** |
方法的内部实现完全一致;重点看@CacheConfig
中的属性值
上面介绍了两种使用不同缓存的姿势:
我们写个简单的验证上面两个CacheManager表示不同缓存的测试用例
1 |
|
操作步骤:
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
@Cacheable
注解,来实现内部缓存的使用姿势首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1 | <dependencies> |
SpringBoot官方对Caffeine的集成,提供了非常好的支持,比如本文介绍的在使用 @Cacheable
注解来处理缓存时,我们无需额外操作,直接在配置文件来实现缓存的指定,以及对应的Caffeine相关配置限定
核心配置如下 application.yml
1 | # 指定全局默认的缓存策略 |
上面的 spring.cache.type 主要用来表明缓存注解的具体缓存实现为 Caffeine,当然还可以是Guava、redis等
其次就是 spring.cache.caffeine.spec
, 它指定了Caffeine的初始化容量大小,最大个数,失效时间等 (无特殊场景时,所有的缓存注解都是公用这个配置的)
首先在启动类上添加 @EnableCaching
注解,注意若不加则缓存不会生效
1 |
|
我们定义一个UserService,主要是用来操作用户相关信息,现在先定义一个User
实体类
1 |
|
然后添加增删查
1 |
|
上面分别介绍了三个注解
其次在类上还有一个@CacheConfig
注解,主要定义了一个 cacheNames
属性,当我们使用缓存注解时,需要注意的是这个cacheNames必须得有,否则就会报错
当一个类中所有缓存公用一个cacheNames时,可以直接在类上添加@CacheConfig
来避免在每个地方都添加指定
1 |
|
我们来实际看一下,第一次没有数据时,返回的是不是空;当有数据之后,缓存是否会命中
这篇博文主要介绍了SpringBoot如何整合Caffeine,结合Spring的缓存注解,基于可以说是很低成本的就让我们的方法实现缓存功能,但是请注意,有几个注意点
另外,查看本文推荐结合下面几篇博文一起享用,以获取更多的知识点
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
本文将简单介绍下Caffeine的使用姿势
首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1 | <dependencies> |
引入上面的jar包之后,就可以进入caffeine的使用环节了;我们主要依照官方wiki来进行演练
caffeine提供了四种缓存策略,主要是基于手动添加/自动添加,同步/异步来进行区分
其基本使用姿势于Guava差不多
1 | private LoadingCache<String, Integer> autoCache; |
注意参数设置,我们先看一下失效策略,共有下面几种
权重:
时间:
引用:
弱引用:这允许在GC的过程中,当没有被任何强引用指向的时候去将缓存元素回收
软引用:在GC过程中被软引用的对象将会被通过LRU算法回收
接下来我们看一下手动方式的使用
1 | public void getUid(String session) { |
查询缓存&添加缓存
getIfPresent(key)
: 不存在时,返回nullget(key, (key) -> {value初始化策略})
: 不存在时,会根据第二个lambda表达式来写入数据,这个就表示的是手动加载缓存asMap
: 获取缓存所有数据添加缓存
put(key, val)
: 主动添加缓存清空缓存
invalidate
: 主动移除缓存invalidateAll
: 失效所有缓存执行完毕之后,输出日志:
1 | 查看缓存! 当没有的时候返回的是 uid: null |
在创建的时候,就指定缓存未命中时的加载规则
1 | // 在创建时,自动指定加载规则 |
它的配置,与前面介绍的一致;主要的区别点在于build时,确定缓存值的获取方式
1 | public void autoGetUid(String session) { |
与前面的区别在于获取缓存值的方式
实际输出结果如下
1 | 自动加载,没有时返回: null |
异步,主要是值在获取换粗内容时,采用的异步策略;使用与前面没有什么太大差别
1 | // 手动异步加载缓存 |
1 | public void asyncGetUid(String session) throws ExecutionException, InterruptedException { |
与前面相比,使用姿势差不多,唯一注意的是,获取的并不是直接的结果,而是CompletableFuture,上面执行之后的输出如下:
1 | 查看缓存! 当没有的时候返回的是 uid: null |
在定义缓存时,就指定了缓存不存在的加载逻辑;与第二个相比区别在于这里是异步加载数据到缓存中
1 | private AtomicInteger idGen; |
1 | public void asyncAutoGetUid(String session) { |
输出:
1 | 自动加载,没有时返回: null |
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
SpringBoot内置了对Liquibase的支持,在项目中使用非常简单
首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1 | <dependencies> |
本文使用MySql数据库, 版本8.0.31; Liquibase的核心依赖liquibase-core
,版本推荐使用SpringBoot配套的版本,一般来讲无需特殊指定
配置文件 resources/application.yml
1 | # 默认的数据库名 |
关键配置为 spring.liquibase.change-log
和 spring.liquibase.enabled
第一个指定的是change-log对应的xml文件,其内容如下
liquibase核心xml文件 resources/liquibase/master.xml
1 |
|
上面的xml依赖了一个xml文件,如第一个主要定义的是初始化的表结构
resources/changelog/000_initial_schema.xml
对应的内容如下
1 |
|
在上面的配置文件中,核心点在 <changeSet>
其中id要求全局唯一,sqlFile
表示这次变动对应的sql语句; 一个<changeSet>
对应一次变更,注意每次变更完成之后,不能再修改(sql文件内容不能改),changeSet本身也不要再去修改
接下来再看一下对应的sql文件
resources/liquibase/data/init_schema_221209.sql
对应的schema相关的表结构定义如下
1 | CREATE TABLE `user` |
resources/liquibase/data/init_data_221209.sql
对应的初始化数据定义如下
1 | INSERT INTO `user` (id, third_account_id, `user_name`, `password`, login_type, deleted) |
上面配置完毕之后,再主项目结构工程中无需特殊处理,我们写一个简单的启动测试一下
1 | 4j |
直接执行之后看一下输出结果(再执行之前,请确保数据库已经创建成功了;若没有则会抛异常)
上面演示的是初始化过程;再实际开发过程中,若存在增量的变更,比如现在需要新增一个测试数据,此时我们的操作流程可以如下
再liquibase/
目录下新增一个001_change_schema.xml
文件,后续的增量变更相关的ChangeSet
都放在这个xml文件中;再master.xml文件中,添加上面xml文件的引入
1 | <include file="liquibase/changelog/001_change_schema.xml" relativeToChangelogFile="false"/> |
其次就是 resources/liquibase/changelog/001_change_schema.xml
文件内容
1 |
|
上面的changeSet
中包含初始化相关的sql文件,内容如下
1 | INSERT INTO `user` (id, third_account_id, `user_name`, `password`, login_type, deleted) |
再次启动验证一下,是否增加了新的数据
本文主要介绍的是SpringBoot如何结合Liquibase来实现数据库版本管理,核心知识点介绍得不多,再实际使用的时候,重点注意
每次变更,都新增一个 <changeSet>
,且保证所有的id唯一;当变更完成之后,不要再修改对应sql文件内容
liquibase本身也有一些相关的知识点,如版本回滚,标签语义等,下篇博文再专门介绍Liquibase本身的核心知识点
如对项目启动之后数据初始话相关有兴趣的小伙伴,欢迎查看
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
接下来介绍一下如何使用DataSourceInitializer来实现自主可控的数据初始化
首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1 | <dependencies> |
本文使用MySql数据库, 版本8.0.31
注意实现初始化数据库表操作的核心配置就在下面,重点关注
配置文件: resources/application.yml
1 | # 默认的数据库名 |
注意上面的配置,我们新定义了一个数据库的配置项 database.name
, 主要是为了检测database是否存在,若不存在时,创建对应的数据库时使用
接下来是初始化sql脚本
resources/init-schema.sql
对应的初始化ddl
1 | CREATE TABLE `user` |
resources/init-data.sql
对用的初始化dml
1 | INSERT INTO `user` (id, third_account_id, `user_name`, `password`, login_type, deleted) |
1 | 4j |
我们这里主要是借助 DataSourceInitializer 来实现初始化,其核心有两个配置
addScripts
来指定对应的sql文件接下来重点需要看的就是needInit方法,我们再这个方法里面,需要判断数据库是否存在,若不存在时,则创建数据库;然后再判断表是否存在,以此来决定是否需要执行初始化方法
1 | /** |
上面的数据库判断是否存在以及初始化的过程相对基础,直接使用了基础的Connection进行操作;这里借助了SpringUtil来获取配置信息,对应的类源码如下
1 | package com.git.hui.schema.config; |
到此整个初始化相关的配置已经完成;接下来我们验证一下
再项目启动成功之后,查看一下数据
1 | 4j |
本文主要介绍的是基于DataSourceInitializer
来实现自主可控的数据初始化,其核心配置为
addScripts
来指定对应的sql文件此外本文还介绍了如何判断数据库是否存在,当数据库不存在时,借助基础的Connection来建立连接,创建数据库;从初始化角度来看,这几篇文中介绍的方式已经足够,但是在项目制的场景下,我们需要记录数据库的版本迭代记录,下一篇将介绍如何使用liquibase来实现数据版本管理,解决初始化以及增量的迭代变更
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
spring.datasource
来实现项目启动之后的数据库初始化,本文作为数据库初始化的第二篇,将主要介绍一下,如何使用spring.jpa
的配置方式来实现相同的效果首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1 | <dependencies> |
本文使用MySql数据库, 版本8.0.31
注意实现初始化数据库表操作的核心配置就在下面,重点关注
配置文件: resources/application.yml
1 | # 默认的数据库名 |
注意上面jpa的一个配置,其次就是上一篇博文中介绍的 spring.datasource.initialization-mode
同样需要将配置设置为 always
使用jpa的配置方式,将ddl-auto
设置为create
或者create-drop
时,会自动搜索@Entity
实体对象,并创建为对应的表
接下来上面的工作准备完毕之后,我们先创建一个实体对象
1 |
|
接下来我们的目标就是基于上面这个实体类生成对应的表结构
1 | 4j |
直接启动项目之后,到数据库中将可以查到已经创建了一个库user3
上面的过程只是初始化了表结构,如果我们希望导入一些初始化数据,可以怎么办?
如上面的配置: spring.jpa.hibernate.ddl-auto: update
,此时在资源目录下,新建 data.sql
, 取值为
1 | INSERT INTO `user3` (id, third_account_id, `user_name`, `password`, login_type, deleted) |
然后再次执行,既可以看到db中会新增一条数据
若spring.jpa.hibernate.ddl-auto: create
,则再资源目录下,新建import.sql
文件,来实现数据初始化
使用Jpa的配置方式,总体来说和前面的介绍的spring.datasource的配置方式差别不大,jpa方式主要是基于@Entity
来创建对应的表结构,且不会出现再次启动之后重复建表导致异常的问题(注意如上面data.sql中的数据插入依然会重复执行,会导致主键插入冲突)
本文中需要重点关注的几个配置:
本文作为数据初始化第二篇,推荐与前文对比阅读,收获更多的知识点 【DB系列】 数据库初始化-datasource配置方式
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
本文将作为初始化方式的第一篇:基于SpringBoot的配置方式实现的数据初始化
首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
1 | <dependencies> |
本文使用MySql数据库, 版本8.0.31
注意实现初始化数据库表操作的核心配置就在下面,重点关注
配置文件: resources/application.yml
1 | # 默认的数据库名 |
上面的配置中,相比较于普通的数据库链接配置,多了几个配置项
上面指定了两个sql,一个是用于建表的ddl,一个是用于初始化数据的dml
resources/config-schema.sql
文件对应的内容如下
1 | CREATE TABLE `user2` |
resources/config-data.sql
文件对应的内容如下
1 | INSERT INTO `user2` (id, third_account_id, `user_name`, `password`, login_type, deleted) |
接下来上面的工作准备完毕之后,在我们启动项目之后,正常就会执行上面的两个sql,我们写一个简单的验证demo
1 | 4j |
从上面的过程走下来,看起来很简单,但是在实际的使用过程中,很容易遇到不生效的问题,下面记录一下
当库表已经存在时,此时我们可能并没有上文中的config-schema.sql
文件,此时对应的配置可能是
1 |
|
如上面所示,当我们只指定了data时,会发现data对应的sql文件也不会被执行;即要求schema对应的sql文件也必须同时存在
针对上面这种情况,可以考虑将data.sql中的语句,卸载schema.sql中
在SpringBoot2.5+版本,使用 spring.sql.init
代替上面的配置项
1 | # springboot 2.5+ 版本使用下面这个 |
相关的配置参数说明如下
spring.sql.init.enabled
:是否启动初始化的开关,默认是true。如果不想执行初始化脚本,设置为false即可。通过-D的命令行参数会更容易控制。spring.sql.init.username
和spring.sql.init.password
:配置执行初始化脚本的用户名与密码。这个非常有必要,因为安全管理要求,通常给业务应用分配的用户对一些建表删表等命令没有权限。这样就可以与datasource中的用户分开管理。spring.sql.init.schema-locations
:配置与schema变更相关的sql脚本,可配置多个(默认用;分割)spring.sql.init.data-locations
:用来配置与数据相关的sql脚本,可配置多个(默认用;分割)spring.sql.init.encoding
:配置脚本文件的编码spring.sql.init.separator
:配置多个sql文件的分隔符,默认是;spring.sql.init.continue-on-error
:如果执行脚本过程中碰到错误是否继续,默认是false`当配置完之后发,发现sql没有按照预期的执行,可以检查一下spring.datasource.initialization-mode
配置是否存在,且值为always
同样上面的项目,在第一次启动时,会执行schema对应的sql文件,创建表结构;执行data对应的sql文件,初始化数据;但是再次执行之后就会报错了,会提示表已经存在
即初始化是一次性的,第一次执行完毕之后,请将spring.datasource.initialization-mode
设置为none
本文主要介绍了项目启动时,数据库的初始化方式,当然除了本文中介绍的spring.datasource
配置之外,还有spring.jpa
的配置方式
对于配置方式不太友好的地方则在于不好自适应控制,若表存在则不执行;若不存在则执行;后面将介绍如何使用DataSourceInitializer
来实现自主可控的数据初始化,以及更现代化一些的基于liquibase的数据库版本管理记录
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
BeanDefinitionRegistryPostProcessor
接口的两个方法,来实现自定义的bean定义,或者对已注册的bean进行修改or代理替换本文将带来的知识点如下:
postProcessBeanDefinitionRegistry
方法 优先于 postProcessBeanFactory
方法执行本文创建的实例工程采用SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ idea
进行开发
具体的SpringBoot项目工程创建就不赘述了,核心的pom文件,无需额外的依赖; 配置文件 application.yml
, 也没有什么特殊的配置
说明
2.2.1.RELEASE
版本进行实测;实际上这些基础的扩展点,在更高的版本中表现也不会有太大的变动,基本上可以无修改复现有关注过博主一灰灰的朋友,应该在我之前的文章中可以翻到bean的动态注册的内容,其中其实也介绍到通过BeanDefinitionRegistryPostProcessor
来实现bean的动态注册,有兴趣的小伙伴可以翻一下,链接如下
接下来我们开始进入正题
现在我们定义一个普通的bean对象,也定义了几个常见的bean初始化之后的回调方法,顺带验证两个知识点
1 | public class DemoBean implements InitializingBean { |
再定义一个bean,构造方法依赖其他的bean
1 | public class DemoBeanWrapper extends DemoBean { |
接下来我们再看一下这两个bean如何进行注册
1 |
|
bean的注册从上面的代码来看比较简单,先看DemoBean的注册
方法: postProcessBeanDefinitionRegistry
在这个方法中进行简单的bean注册,除了上面这个稍显复杂的注册方式之外,也可以使用更简单的策略,如下,省略掉BeanDefinitionBuilder.genericBeanDefinition
第二个参数
1 | BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(DemoBean.class); |
这个方法内的bean注册,更适用于简单的bean对象注册,如当其构造方法依赖其他的bean时,放在这个方法中好像没辙,此时则放在第二个方法中就更合适了
方法: postProcessBeanFactory
这个方法的参数是BeanFactory,可以通过它获取其他的bean对象,因此适用于DemoBeanWrapper的注册了,当然除了上面的使用姿势之外,也可以如下
1 |
|
单独看上面的代码可能对知识点理解不够直观清晰,那么我们就进行知识点归纳一下
bean注册方式
如何生成Bean的定义 BeanDefinition
?
1 | // 1. bean定义构造器 |
两个方法的选择
postProcessBeanDefinitionRegistry
方法执行先于 postProcessBeanFactory
postProcessBeanDefinitionRegistry
在bean实例化之前触发,可用于注册简单的自定义bean对象postProcessBeanFactory
: 若bean的定义中需要依赖其他的bean对象,则放在这个方法内实现,通过BeanFactory参数获取其他bean文章开头介绍了除了自定义bean之外,还可以做一些其他的操作,如针对现有的bean定义进行修改,下面给一个基础的demo,针对一个已有的bean,设置它的init方法
新增一个普通的bean对象
1 |
|
然后我们通过修改bean注册,来指定bean加载完之后,执行init方法,在前面的AutoBeanDefinitionRegistryPostProcessor
中进行扩展
1 |
|
然后我们将整个项目执行以下,看下会输出些啥
1 | ========> postProcessBeanDefinitionRegistry ---> |
从上面的输出也可以看出,我们的几个自定义bean都被正常的加载、注入,依赖使用也没有什么问题;而且从日志输出还可以看出bean初始化后的触发方法,也有先后顺序
@PostConstruct
> InitializingBean#afterPropertiesSet
> init-method
(这个可以理解为xml定义bean中的初始化方法, @Bean注解中的initMethod)最后进入大家喜闻乐见的知识点汇总环节,本文中主要介绍的是bean定义加载之后、实例化之前的扩展点BeanDefinitionRegistryPostProcessor
通过它,我们可以实现自定义的bean注册,也可以实现对现有的bean定义进行扩展修改;有两个方法
postProcessBeanDefinitionRegistry
postProcessBeanFactory
bean注册方式
如何生成Bean的定义 BeanDefinition
?
1 | // 1. bean定义构造器 |
看完本文之后,勤于思考的小伙伴可能就会想,这个东西到底有啥用,有真实的应用场景么?
自定义bean注册实例场景
这个应用场景就非常的典型了,用过mybatis的小伙伴都知道,我们会定义一个Mapper接口,用于与对应的xml文件进行映射,那么这些mapper接口是怎么注册到Spring容器的呢?
org.mybatis.spring.mapper.MapperScannerConfigurer
BeanDefinitionRegistryPostProcessor
与ClassPathBeanDefinitionScanner
来实现扫描相关的类,并注册beanbean定义修改实例场景
对于已有的bean定义进行修改,同样也有一个应用场景,在SpringCloud中,有个RefreshAutoConfiguration#RefreshScopeBeanDefinitionEnhancer
它会捞出HikariDataSource
数据源bean对象,添加RefreshScope
的能力增强,支持配置文件的动态加载
从而实现数据源配置的热加载更新(不发版,直接改数据库连接池,是不是很方便?)
我们知道在bean创建之后执行某些方法有多种策略,那么不同的方式先后顺序是怎样的呢?
bean创建到销毁的先后执行顺序如下
本文为Spring扩展点系列中的第二篇,接下来的扩展知识点同样是bean定义之后,实例化之前的BeanFactoryPostProcessor
,那么这两个究竟又有什么区别呢? 应用场景又有什么区别呢?我是一灰灰,欢迎关注我的Spring专栏,咱们下文见
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
在Spring的启动过程中,一系列的操作步骤中,提供了很多的扩展点,供我们来增强;简单来说就是提供了很多的钩子,这样当我们在某个节点执行前后,想干点其他的事情时,可以很简单的支持;本文介绍的ApplicationContextInitializer
,spring容器在刷新之前会回调这个接口,从而实现在spring容器未初始化前,干一些用户希望做的事情
本文创建的实例工程采用SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ idea
进行开发
具体的SpringBoot项目工程创建就不赘述了,核心的pom文件,无需额外的依赖
配置文件 application.yml
, 也没有什么特殊的配置
源码工程参考文末的源码
当我们希望实现一个自定义的上下文初始化时,非常简单,实现上面这个接口就行了,如
1 | public class ApplicationContextInitializer01 implements ApplicationContextInitializer { |
上面自定义一个扩展点,如何使它生效呢?
官方提供了三种方式,如在启动时,直接进行注册: springApplication.addInitializers(new ApplicationContextInitializer01());
1 |
|
当我们的扩展点是放在一个jar包中对外提供时,使用上面的启动注册方式显然是不可行的,此时更推荐的做法就是通过Spring的SPI机制进行注册
在资源目录下的META-INF/spring.factories
文件中进行注册
1 | org.springframework.context.ApplicationContextInitializer=com.git.hui.extention.context.ApplicationContextInitializer02 |
说明
AutoConfiguration
的注册通常也是使用这种方式除了上面的两种注册方式之外,另外还有一个配置文件的方式,在配置文件application.properties
或 application.yml
中,如下配置
1 | context: |
启动测试
上面三种注册方式,我们实现三个自定义的扩展点,然后启动之后,看一下实际输出
上面的输出,可以简单的得出一个结论,不同注册方式的优先级(为了更合理的验证下面的观点,推荐大家修改下上面三个自定义扩展点名,排除掉是因为扩展名导致的排序问题)
对于自定义的扩展点实现,当存在顺序关系时,我们可以通过@Order
注解来实现, 如当上面的三个扩展点都是通过启动方式注册时
1 | 5) ( |
输出实例如下
接着重点来了
@Order
注解修饰的顺序,并不能打破 配置文件 > SPI > 启动方式注册的顺序关于自定义实现类的执行顺序,规则如下
@Order
注解进行修饰,值越小则优先级越高最后我们再来看一下,这个扩展点到底有什么用,我们再什么场景下会用到这个呢?
一个经常可以看到的应用场景如通过它来指定需要激活的配置文件
1 | public class ApplicationContextInitializer03 implements ApplicationContextInitializer { |
但是一般也很少见到有人这么干,因为直接使用配置参数就行了,那么有场景需要这么做么?
答案当然是有的,比如现在广为流行的docker容器部署,当我们希望每次都是打同一个镜像,然后在实际运行的时候,根据不同的环境来决定当前镜像到底启用哪些配置文件,这时就有用了
比如我们通过容器的环境参数 app.env
来获取当前运行的环境,如果是prod,则激活application-prod.yml
; 如果是test,则激活application-test.yml
那么此时可以这么干
1 | public class EenvActiveApplicationContextInitializer implements ApplicationContextInitializer { |
本文作为扩展点的第一篇,通过实现ApplicationContextInitializer
接口,从而达到在spring容器刷新之前做某些事情的目的
通常自定义的ApplicationContextInitializer有三种注册方式,按照优先级如下
@Order
注解来指定优先级,值越小优先级越高最后还给出了一个可以应用得实例场景,即如何实现一个镜像在不同的环境中启动运行
下一个扩展点我们将介绍如何通过BeanDefinitionRegistryPostProcessor
来实现非Spring生态的Bean加载使用
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
本文节选自 《实战演练专题》
通过一个小的业务点出发,搭建一个可以实例使用的项目工程,将各种知识点串联起来; 实战演练专题中,每一个项目都是可以独立运行的,包含若干知识点,甚至可以不做修改直接应用于生产项目;
今天的实战项目主要解决的业务需求为:每日新增用户统计,生成报表,并邮件发送给相关人
本项目将包含以下知识点:
需要相对来说属于比较明确的了,目的就是实现一个自动报表统计的任务,查询出每日的用户新增情况,然后推送给指定的用户
因此我们将很清晰的知道,我们需要干的事情
定时任务
这里重点放在如何来支持这个任务的定时执行,通常来说定时任务会区分为固定时刻执行 + 间隔时长执行两种(注意这种区分主要是为了方便理解,如每天五点执行的任务,也可以理解为每隔24h执行一次)
前者常见于一次性任务
,如本文中的每天统计一次,这种就是相对典型的固定时刻执行的任务;
后者常见于轮询式任务
,如常见的应用探活(每隔30s发一个ping消息,判断服务是否健在)
定时任务的方案非常多,有兴趣的小伙伴可以关注一波“一灰灰blog”公众号,蹲守一个后续
本文将直接采用Spring的定时任务实现需求场景,对这块不熟悉的小伙伴可以看一下我之前的分享的博文
每日新增用户统计
每日新增用户统计,实现方式挺多的,比如举几个简单的实现思路
上面两个方案都需要借助额外的库表来辅助支持,本文则采用直接统计用户表,根据注册时间来聚合统计每日的新增用户数
关于如何使用mysql进行统计每日新增用户,不熟悉的小伙伴,推荐参考博主之前的分享文章
报表生成&推送用户
接下来就是将上面统计的数据,生成报表然后推送给用户;首先是如何将数据生成报表?其次则是如何推送给指定用户?
将数据组装成报表的方式通常取决于你选择的推送方式,如飞书、钉钉之类的,有对应的开发api,可以直接推送富文本;
本文的实现姿势则选择的是通过邮件的方式进行发送,why?
对于邮件,大家应该都有,无论是qq邮箱,还是工作邮箱;基本上对于想要直接跑本文的小伙伴来说,没有什么额外的门槛
关于java/spring如何使用邮箱,对此不太熟悉的小伙伴,可以参考博主之前的分享文章
上面文章中介绍的是FreeMaker来实现模板渲染,本文则介绍另外一个知识点,借助Thymleaf来实现数据报表的生成 (一篇文章获取这么多知识点,就问你开不开心O(∩_∩)O)
首选搭建一个基本的SpringBoot应用,相信这一步大家都很熟悉了;若有不懂的小伙伴,请点赞、评论加博主好友,手把手教你,不收费
最终的项目依赖如下
1 | <dependencies> |
别看上面好像依赖了不少包,实际上各有用处
spring-boot-starter-web
: 提供web服务spring-boot-starter-mail
: 发邮件就靠它mybatis-spring-boot-starter
: 数据库操作我们的用户存在mysql中,这里使用mybatis来实现db操作(又一个知识点来了,收好不谢)
文末的源码包含库表结构,初始化数据,可以直接使用
既然模拟的是从数据库中读取每日新增用户,所以我们准备了一张表
1 | CREATE TABLE `u1` ( |
接下来准备写入一些数据;为了模拟某些天没有新增用户,贴心的一灰灰博主给大家提供基于python的数据生成脚本,源码如下 (python3+,对python不熟的小伙伴,可以到博主的站点进补一下,超链)
1 | import datetime |
数据准备完毕之后,接下来配置一下db、email相关的参数
resources/application.yml
文件内容如下
1 | spring: |
上面的配置分为三类
接下来就正式进入大家喜闻乐见的编码实现环节,我们直接使用mybaits来实现数据库操作,定义一个统计的接口
1 | /** |
接口中定义了一个PO对象,就是我们希望返回的数据,其定义就非常清晰简单了,时间 + 数量
1 |
|
上面定义的知识接口,具体首先,当然是放在mybatis的传统xml文件中,根据前面application.yml配置,我们的xml文件需要放在 resources/mapper
目录下,具体实现如下
1 |
|
重点看一下上面的sql实现,为什么会一个join逻辑?
那我们稍稍思考,若我们直接通过日期进行format之后,再group一下统计计数,会有什么问题?给大家3s的思考时间
好的3s时间到,现在公布答案,当某一天一个新增用户都没有的时候,会发生什么事情?会出现这一天的数据空缺,即返回的列表中,少了一天,不连续了,如果前段的小伙伴基于这个列表数据进行绘图,很有可能出现异常
所以出于系统的健壮性考虑(即传说中的鲁棒性),我们希望若某一天没有数据,则对应的计数设置为0
具体的sql说明就不展开了,请查看博文获取更多: MySql按时、天、周、月进行数据统计
数据统计出来之后,接下来就是基于这些数据来生成我们报表,我们借助Thymleaf来实现,因此先写一个html模板,resources/templates/report.html
1 |
|
一个非常简单的table模板,需要接收三个数据,与之对应的vo对象,我们定义如下
1 |
|
接下来就是拿到数据之后,将它与模板渲染得到我们希望的数据,这里主要借助的是org.thymeleaf.spring5.SpringTemplateEngine
核心实现如下
1 |
|
模板渲染就一行templateEngine.process("report", context)
,第一个参数为模板名,就是上面的html文件名(对于模板文件、静态资源怎么放,放在那儿,这个知识点当然也可以在一灰灰的站点获取,超链)
第二个参数用于封装上下文,传递模板需要使用的参数
报表生成之后,就是将它推送给用户,我们这里选定的是邮箱方式,具体实现也比较简单,但是在最终部署到生产环境(如阿里云服务器时,可能会遇到坑,同样明显的知识点,博主会没有分享么?当然不会没有了,Email生产环境发送排雷指南,你值得拥有)
1 | /** |
上面的实现,直接写死了收件人邮箱,即我本人的邮箱,各位大佬在使用的时候,请记得替换一下啊
上面的实现除了发送邮件这个知识点之外,还有一个隐藏的获取配置参数的知识点,即environment#getProperty()
,有兴趣的小伙伴翻博主的站点吧
上面几部基本上就把我们的整个任务功能都实现了,从数据库中统计出每日新增用户,然后借助Thymleaf来渲染模板生成报告,然后借助email进行发送
最后的一步,就是任务的定时执行,直接借助Spring的Schedule来完成我们的目标,这里我们希望每天4:15分执行这个任务,如下配置即可
1 | // 定时发送,每天4:15分统计一次,发送邮件 |
最后测试演练一下,启动方法如下,除了基本的启动注解之外,还指定了mapper接口位置,开启定时任务;感兴趣的小伙伴可以试一下干掉这两个注解会怎样,评论给出你的实测结果吧
1 |
|
当然我再实际测试的时候,不可能真等到早上四点多来看是否执行,大晚上还是要睡觉的;因此本地测试的时候,可以将上面定时任务改一下,换成每隔一分钟执行一次
接一个debug的中间图
打开的内容展示
此外,源码除了实现了定时推送之外,也提供了一个web接口,访问之后直接可以查看报表内容,方便大家调样式,实现如下
1 |
|
最后进入一灰灰的保留环节,这么“大”一个项目坐下来的,当然是得好好盘一盘它的知识点了,前面的各小节内容中有穿插的指出相应的知识点,接下来如雨的知识点将迎面袭来,不要眨眼
除了上面比较突出的知识点之外,当然还有其他的,如Spring如何读取配置参数,SpringMVC如何向模板中传递上下文,模板语法,静态资源怎么放等等
写到这我自己都惊呆了好么,一篇文章这么多知识点,还有啥好犹豫的,一键三连走起啊,我是一灰灰,这可能是我这个假期内最后一篇实战干货了,马上要开学了,老婆孩子回归之后,后续的更新就靠各位读友的崔更保持了
本文中所有知识点,都可以在我的个人站点获取,欢迎关注: https://hhui.top/
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
答案当然是可行的,接下来我们将介绍一下,一个接口的返回数据类型,可以怎么处理
本文创建的实例工程采用SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ idea
进行开发
具体的SpringBoot项目工程创建就不赘述了,对于pom文件中,需要重点关注下面两个依赖类
1 | <dependencies> |
注意 jackson-datafromat-xml
这个依赖,加上这个主要时为了支持返回xml格式的数据
正常来讲,一个RestController的接口,默认返回的是Json格式数据,当我们引入了上面的xml包之后,会怎样呢?返回的还是json么?
如果一个接口希望返回json或者xml格式的数据,最容易想到的方式就是直接设置RequestMapping
注解中的produce属性
这个值主要就是用来设置这个接口响应头中的content-type
; 如我们现在有两个接口,一个指定返回json格式数据,一个指定返回xml格式数据,可以如下写
1 |
|
上面的实现中
produces = application/xml
produces = applicatin/json
接下来我们访问一下看看返回的是否和预期一致
从上面截图也可以看出,xml接口返回的是xml格式数据;json接口返回的是json格式数据
上面的方式,非常直观,自然我们就会有一个疑问,当接口上不指定produces属性时,直接访问会怎么表现呢?
1 | "/") (path = |
请注意上面的截图,两种访问方式返回的数据类型不一致
application/xhtml+xml
响应头的数据(实际上还是xml格式)那么问题来了,为什么两者的表现形式不一致呢?
对着上面的图再看三秒,会发现主要的差别点就在于请求头Accept
不同;我们可以通过这个请求头参数,来要求服务端返回我希望的数据类型
如指定返回json格式数据
1 | curl 'http://127.0.0.1:8080' -H 'Accept:application/xml' -iv |
从上面的执行结果也可以看出,返回的类型与预期的一致;
说明
请求头可以设置多种MediaType,用英文逗号分割,后端接口会根据自己定义的produce与请求头希望的mediaType取交集,至于最终选择的顺序则以accept中出现的顺序为准
看一下实际的表现来验证下上面的说法
通过请求头来控制返回数据类型的方式可以说是非常经典的策略了,(遵循html协议还有什么好说的呢!)
除了上面介绍的两种方式之外,还可以考虑为所有的接口,增加一个根据特定的请求参数来控制返回的类型的方式
比如我们现在定义,所有的接口可以选传一个参数 mediaType
,如果值为xml,则返回xml格式数据;如果值为json,则返回json格式数据
当不传时,默认返回json格式数据
基于此,我们主要借助mvc配置中的内容协商ContentNegotiationConfigurer
来实现
1 |
|
上面的实现中,添加了很多注释,先别急;我来逐一进行说明
1 | .parameterName("mediaType") |
上面这三行代码,主要就是说,现在可以根据传参 mediaType 来控制返回的类型,我们新增一个接口来验证一下
1 | "param") (path = |
我们来看下几个不同的传参表现
1 | # 返回json格式数据 |
疑问:若请求头中传递了Accept或者接口上定义了produce,会怎样?
当指定了accept时,并且传参中指定了mediaType,则以传参为准
accept: application/json,application.xml
, 此时mediaType=json
, 返回json格式accept: application/json
, 此时 mediaTyep=xml
, 返回xml格式accept: text/html
,此时mediaType=xml
,此时返回的也是xml格式accept: text/html
,此时mediaType
不传时 ,因为无法处理text/html
类型,所以会出现406accept: application/xml
, 但是mediaType
不传,虽然默认优先是json,此时返回的也是xml格式,与请求头希望的保持一致但是若传参与produce冲突了,那么就直接406异常,不会选择mediaType设置的类型
produce = applicatin/json
, 但是 mediaType=xml
,此时就会喜提406细心的小伙伴可能发现了上面的配置中,注释了一行 .ignoreAcceptHeader(true)
,当我们把它打开之后,前面说的Accept请求头可以随意传,我们完全不care,当做没有传这个参数进行处理即可开
本文介绍了三种方式,控制接口返回数据类型
方式一
接口上定义produce, 如 @GetMapping(path = "p2", produces = {"application/xml", "application/json"})
注意produces属性值是有序的,即先定义的优先级更高;当一个请求可以同时接受xml/json格式数据时,上面这个定义会确保这个接口现有返回xml格式数据
方式二
借助标准的请求头accept,控制希望返回的数据类型;但是需要注意的时,使用这种方式时,要求后端不能设置ContentNegotiationConfigurer.ignoreAcceptHeader(true)
在实际使用这种方式的时候,客户端需要额外注意,Accept请求头中定义的MediaType的顺序,是优于后端定义的produces顺序的,因此用户需要将自己实际希望接受的数据类型放在前面,或者干脆就只设置一个
方式三
借助ContentNegotiationConfigurer
实现通过请求参数来决定返回类型,常见的配置方式形如
1 | configurer.favorParameter(true) |
即添加这个设置之后,最终的表现为:
produce
中支持的求交集,优先级则按照defaultContentType中定义的顺序来选择注意注意:当配置中忽略了AcceptHeader时,.ignoreAcceptHeader(true)
,上面第三条作废
最后的最后,本文所有的源码可以再下面的git中获取;本文的知识点已经汇总在《一灰灰的Spring专栏》 两百多篇的原创系列博文,你值得拥有;我是一灰灰,咱们下次再见
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛