开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

springboot从数据库中获取application配置

发表于 2021-11-16

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战

在一次开发中,领导提供了一个需求,将springboot配置文件的值存放在数据库中,并且能否动态更改。在调用后,决定先做了一个初版。

1.实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
js复制代码import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Map;

import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.origin.OriginTrackedValue;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;


public class ConfigureListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {

@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
Connection conn = null;
Statement st = null;
ResultSet rs = null;
// 获取spring Environment
MutablePropertySources propertySources = event.getEnvironment().getPropertySources();
// 配置放在了application-pro或者是application-dev 中 赋值复制需要在其中赋值
for (PropertySource<?> propertySource : propertySources) {
boolean applicationConfig = propertySource.getName().contains("application-");
if (!applicationConfig) {
continue;
}
// 获取上文的application集合中获取数据库连接
Map<String, OriginTrackedValue> dataBaseSource =
(Map<String, OriginTrackedValue>)propertySource.getSource();
String driverClass = String.valueOf(dataBaseSource.get("spring.datasource.driver-class-name").getValue());
String url = String.valueOf(dataBaseSource.get("spring.datasource.url").getValue());
String user = String.valueOf(dataBaseSource.get("spring.datasource.username").getValue());
String password = String.valueOf(dataBaseSource.get("spring.datasource.password").getValue());
// 因为在spring初始化之前 所有不能使用注解 所以需要jdbc直接连接数据库 首先建立驱动
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
conn = DriverManager.getConnection(url, user, password);
// 1、获取连接对象
// 2、创建statement类对象,用来执行SQL语句!!
st = conn.createStatement();
// 3、创建sql查询语句
String sql = "select * from SYS_CONFIGURE";
// 4、执行sql语句并且换回一个查询的结果集
rs = st.executeQuery(sql);
while (rs.next()) {
// 获取数据库中的数据
String item = rs.getString("ITEM");
String itemValue = rs.getString("ITEM_VALUE");
// 通过数据库中的配置 修改application集合中数据
Map<String, OriginTrackedValue> source =
(Map<String, OriginTrackedValue>)propertySource.getSource();
OriginTrackedValue originTrackedValue = source.get(item);
OriginTrackedValue newOriginTrackedValue =
OriginTrackedValue.of(itemValue, originTrackedValue.getOrigin());
source.put(item, newOriginTrackedValue);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
1
2
3
4
5
6
7
8
9
js复制代码@SpringBootApplication
public class TestApplication {

public static void main(String[] args) {
SpringApplication app = new SpringApplication(TestApplication.class);
app.addListeners(new ConfigureListener());
app.run(args);
}
}

2.原理简介

在springboot初始化时,会识别application .xml,并将扫描到的配置类放于MutablePropertySources中,在执行初始化后,将会执行addListeners,在addListeners中重新复写了MutablePropertySources对象。将从数据库中查询到的配置覆盖到已经查询到的对象中,从而实现配置文件的更改。

1
2
js复制代码获取MutablePropertySources 代码。
MutablePropertySources propertySources = event.getEnvironment().getPropertySources();

3.使用中间件

最后该方案只是做了一个demo。建议使用apollo或者nacos作为配置中心。功能更加的强大且更为稳定。何苦自己造轮子呢。。。
​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一篇文带你零基础玩转mysql触发器 超级干货,建议收藏

发表于 2021-11-16

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」

​嗨,家人们,我是bug菌,我又来啦。今天我们来聊点什么咧,OK,接着为大家更Ubuntu系列文章吧。哈哈哈哈,说习惯了,最近有小伙伴私信我,老是Ubuntu都看腻了,没问题,今个儿一周就给大家换个口味,大家可得认真听好好学哈。

具有很好的教学价值,希望小伙伴们根据这篇文章可以有所收获,建议小伙伴们先收藏后阅读哦。

小伙伴们如果觉得文章不错,点赞、收藏、评论,分享走一起呀,记得给bug菌来个一键三连~~

特此呢,针对小白系列教学,bug菌专门开放了一个专栏,感兴趣的朋友可以关注《Ubuntu零基础教学》。

bug菌做这么多只为一件事就是想把你们都教会,教不会不收学费!对你们有所帮助的小伙伴们,还请不忘给bug菌一个赞,你们的鼓励就是对我最大的支持! 那么接下来,干正事啦!bug菌要开始上课了喔~

文章目录:

一、绪论:

二、前言:

三、本章内容:

1. MySQL触发器是什么?

#1基本概念:

#2作用:

#3版本:

2. 为什么要用MySQL触发器?

3. 如何创建MySQL触发器?

#1基本语法

#2触发时间

#3触发事件

#4注意事项

#5实例演示

#6触发器的执行顺序

4. MySQL触发器应用

5. MySQL触发器优缺点

#1优点

#2缺点

四、常用命令

五、个人建议

六、热文推荐

一、环境说明

系统要求:Ubuntu20.04

二、前言

MySQL 作为当今最流行的关系型数据库管理系统之一,由瑞典MySQL AB 公司开发,属于 Oracle 旗下的产品。由于 MySQL已源码,因此大大降低了成本,但也可以从Oracle购买商业许可证版本,以获得高级支持服务(特殊企业用户需要)。

与其他数据库软件(如Oracle数据库或Microsoft SQL Server)相比,MySQL已经非常容易学习和掌握。MySQL可以在各种平台上运行UNIX,Linux,Windows等;也可以将其安装在服务器甚至桌面系统上。 此外,MySQL是可靠,可扩展和快速的。如果您开发网站或Web应用程序,MySQL是一个不错的选择(强烈建议使用)。MySQL是LAMP堆栈的重要组成部分,包括Linux,Apache,MySQL和PHP。更多mysql介绍使用请上官网学习,此处就不多言阐述啦。


奉上MySQL官网址: [MySQL](http://www.mysql.com/) ,MySQL社区版本下载地址: [MySQL :: Download MySQL Community Server](http://dev.mysql.com/downloads/mysql/)

​

三、前言:

想必使用过mysql的小伙伴,都知道,mysql有事务、索引、触发器、存储过程等;那么今天,bug菌今天就来给大家聊一聊它

但是不会鸽太久啦,顶多鸽一两个礼拜…啊呸,一两天啦;此期也是为了允诺私聊bug菌的小伙伴啦,单独更一期mysql触发器的相关基础教学;还请小伙伴们细品~

阅读文章的同时若是发现途中有讲述的不对或者理解有偏差的地方,还请小伙伴们多多谅解,毕竟bug菌也经验水平有限啦,但非常欢迎小伙伴们能留下观点、提出宝贵建议和意见,下方留言评论,找出我的不足,bug菌的成长同时也是见证你的成长!万事尽力!尽力而为。

**如果最后觉得该文章对你有所帮助,还请不要吝啬你的赞哦,直接pia的点亮就完事了啦 up up up!!!**你们的鼓励就是对bug菌写作最大的支持!

那么接下来,干正事啦!bug菌要开始上课了喔~

​

四、本章内容:

  1. MySQL触发器是什么?

#1基本概念:

  • 触发器是一种特殊类型的存储过程,它不同于存储过程,主要是通过事件触发而被执行的,即不是主动调用而执行的;而存储过程则需要主动调用其名字执行。
  • 触发器:trigger,是指事先为某张表绑定一段代码,当表中的某些内容发生改变(增、删、改)的时候,系统会自动触发代码并执行。

#2作用:

  • 可在写入数据前,强制检验或者转换数据(保证护数据安全)。触发器发生错误时,前面用户已经执行成功的操作会被撤销,类似事务的回滚。

#3版本:

  • MySQL 5.0开始才支持存储过程、触发器
  1. 为什么要用MySQL触发器?

  • 触发器可以检查或修改新数据值,这意味着我们可以利用触发器强制实现数据完整性,比如检查某个百分比数值是否在0-100之间,还可以用来对输入数据进行必要的过滤;
  • 触发器可以将表达式的结果赋值给数据列作为默认值。因此我们可以绕开数据列定义里的默认值必须为常数的限制;
  • 触发器可以在删除或修改数据行之前先检查它的当前内容。利用这种能力可以实现许多功能,比如把对现有数据的修改记载到一个日志里。

)​

  1. 如何创建MySQL触发器?

#1基本语法

1
2
3
4
5
6
7
8
sql复制代码delimiter 自定义结束符号
create trigger 触发器名字 触发时间 触发事件 on 表 for each row
begin
-- 触发器内容主体,每行用分号结尾
end
自定义的结束符合

delimiter ;

on 表 for each:触发对象,触发器绑定的实质是表中的所有行,因此当每一行发生指定改变时,触发器就会发生;

其中:

  1. 触发器名称:标识触发器名称,用户自行指定;
  2. 触发时间:标识触发时间,取值为 BEFORE 或 AFTER;
  3. 触发事件:标识触发事件,取值为 INSERT、UPDATE 或 DELETE;
  4. 触发器表名:标识建立触发器的表名,即在哪张表上建立触发器;
  5. 触发器程序体:可以是一句SQL语句,或者用 BEGIN 和 END 包含的多条语句。

由此可见,可以建立6种触发器,即:BEFORE INSERT、BEFORE UPDATE、BEFORE DELETE、AFTER INSERT、AFTER UPDATE、AFTER DELETE。

#2触发时间

当 SQL 指令发生时,会令行中数据发生变化,而每张表中对应的行有两种状态:数据操作前和操作后

  • before:表中数据发生改变前的状态
  • after:表中数据发生改变后的状态

PS:如果 before 触发器失败或者语句本身失败,将不执行 after 触发器(如果有的话)

#3触发事件

触发器是针对数据发送改变才会被触发,对应的操作只有

  • INSERT
  • DELETE
  • UPDATE

#4注意事项

在 MySQL 5 中,触发器名必须在每个表中唯一,但不是在每个数据库中唯一,即同一数据库中的两个表可能具有相同名字的触发器

每个表的每个事件每次只允许一个触发器,因此,每个表最多支持 6 个触发器

before/after insert、before/after delete、before/after update

另外有一个限制是不能同时在一个表上建立2个相同类型的触发器,因此在一个表上最多建立6个触发器。

​

#5实例演示

1、创建完整触发器示例:

我先来先来创建一个名为 trigger_demo 的完整触发器;

目的:当user表发生update 操作后,自动往 log_info表中插入一条日志记录,具体记录 操作时间、更新前的那条数据用户名称、用户id;

1
2
3
4
5
6
sql复制代码delimiter || 
create trigger trigger_demo AFTER UPDATE ON user for each row
BEGIN
INSERT into log_info ( create_time,user_id,user_name )VALUES (now(), old.user_id,old.user_name);
END ||
delimiter;

a、变量详解

MySQL 中使用 delimiter 来定义一局部变量,该变量只能在 BEGIN … END 复合语句中使用,并且应该定义在复合语句的开头,

默认情况下,delimiter是分号 ; ,在命令行客户端中,如果有一行命令以分号结束,那么回车后,mysql将会执行该命令。

1
复制代码delimiter ||

其中DELIMITER 定好结束符为”|| “, 然后最后又定义为”;”, MYSQL的默认结束符为”;”。

b、NEW 与 OLD 详解

上述示例中使用了NEW关键字,和 MS SQL Server 中的 INSERTED 和 DELETED 类似,MySQL 中定义了 NEW 和 OLD,用来表示

触发器的所在表中,触发了触发器的那一行数据。

具体地:

  • 在 INSERT 型触发器中,NEW 用来表示将要(BEFORE)或已经(AFTER)插入的新数据;
  • 在 UPDATE 型触发器中,OLD 用来表示将要或已经被修改的原数据,NEW 用来表示将要或已经修改为的新数据;
  • 在 DELETE 型触发器中,OLD 用来表示将要或已经被删除的原数据;

使用方法: NEW.columnName (columnName 为相应数据表某一列名)

另外,old是只读的,而 new 则可以在触发器中使用 set赋值,这样不会再次触发 触发器,造成循环调用(比如每插入一个用户前,都在其用户code前拼接“20210617”),这就可以使用set 定义一个临时变量了。

如下:定义一个临时变量,一般都以@前缀命名,比如 @new_user_id ;然后在你要执行的事件sql中直接拿来用即可!

1
2
3
4
5
6
7
sql复制代码delimiter || 
create trigger trigger_demo_update AFTER update ON sys_user for each row
BEGIN
SET @new_user_id = CONCAT('20210617',new.id);
INSERT into log_info ( create_time,user_id,user_name )VALUES (now(), @new_user_id,new.user_name);
END ||
delimiter;

)

演示:

  • 测试update获取更新前更新后数据

UPDATE user SET user_name= ‘改名字’,user_id=’999’ where user_id = ‘30’ #对user表执行update操作

delimiter ||
create trigger trigger_demo_update AFTER UPDATE ON user for each row
BEGIN
INSERT into log_info ( create_time,user_id,user_name )VALUES (now(), old.user_id,old.user_name);
INSERT into log_info ( create_time,user_id,user_name )VALUES (now(), new.user_id,new.user_name);
END ||
delimiter;

经测试,更新user后立马执行触发器,可以看到log_info表操作日志插入进去了! 如下是log_info表数据截图。

​

  • 测试insert获取更新后数据

INSERT into user (user_name,user_account) VALUES(‘张三’,’zhangsan’); #对user表执行insert操作

delimiter ||
create trigger trigger_demo_insert AFTER Insert ON user for each row
BEGIN
INSERT into log_info ( create_time,user_id,user_name )VALUES (now(), new.user_id,new.user_name);
END ||
delimiter;

)​

经测试,新增user后立马执行触发器,成功将插入后的数据写入log_info表中,如下是log_info表数据截图。

​

  • 测试delete获取更新后数据

DELETE FROM user where user_old = ‘999’ #对user表执行delete操作

delimiter ||
create trigger trigger_demo_delete AFTER delete ON user for each row
BEGIN
INSERT into log_info ( create_time,user_id,user_name )VALUES (now(), old.user_id,old.user_name);
END ||
delimiter;

)​经测试,删除后立马执行触发器,成功将删除前的数据写入log_info表中,如下是log_info表数据截图。

​

)​

2、查看触发器

和查看数据库(show databases;)查看表格(show tables;)一样,查看触发器的语法如下:

1
2
sql复制代码show triggers;   #查看全部触发器
show create trigger trigger_demo_delete; #查看触发器的创建语句

​

3、删除触发器

和删除数据库、删除表格一样,删除触发器的语法如下:切记:触发器不能修改,只能删除。

1
sql复制代码drop trigger + 触发器名

演示:直接命令删除;

1
sql复制代码DROP trigger trigger_demo_delete; #删除触发器

也可以通过navicat 选择要删除的触发器,点击【删除触发器】摁钮,会弹出二次确认,点击【删除】即可。

​

提问:触发器可以批量删除吗?

回答:经测试,不可以。

​

验证下方删除多个,结果执行失败了!

显而易见,不支持批量删除。

​

单删成功。

​

切记:如果某个触发器不需要用了,一定要立即把这个触发器给删掉,以免造成意外操作,这很关键。切记切记!!!

#6触发器的执行顺序

我们建立的数据库一般都是 InnoDB 数据库,其上建立的表是事务性表,也就是事务安全的。这时,若SQL语句或触发器执行失败,MySQL 会回滚事务,有:

①如果 BEFORE 触发器执行失败,SQL 无法正确执行。

②SQL 执行失败时,AFTER 型触发器不会触发。

③AFTER 类型的触发器执行失败,SQL 会回滚。

)​

  1. MySQL触发器应用

触发器针对的是数据库中的每一行记录,每行数据在操作前后都会有一个对应的状态,触发器将没有操作之前的状态保存到 old 关键字中,将操作后的状态保存到 new 中;

​

因此说明:MySQL 的触发器中不能对本表进行 insert、update 和 delete 操作,否则会报错;

  1. MySQL触发器优缺点

#1优点

  • SQL触发器提供了检查数据完整性的替代方法。
  • SQL触发器可以捕获数据库层中业务逻辑中的错误。
  • SQL触发器提供了运行计划任务的另一种方法。通过使用SQL触发器,您不必等待运行计划的任务,因为在对表中的数据进行更改之前或之后自动调用触发器。
  • SQL触发器对于审核表中数据的更改非常有用。

#2缺点

  • SQL触发器只能提供扩展验证,并且无法替换所有验证。一些简单的验证必须在应用层完成。 例如,您可以使用JavaScript或服务器端使用服务器端脚本语言(如JSP,PHP,ASP.NET,Perl等)来验证客户端的用户输入。
  • 从客户端应用程序调用和执行SQL触发器不可见,因此很难弄清数据库层中发生的情况。
  • SQL触发器可能会增加数据库服务器的开销。

​

四、常用命令

1
2
3
sql复制代码show triggers;   #查看全部触发器
show create trigger [触发器名字]; 查看触发器的创建语句
DROP trigger [触发器名字]; #删除触发器

五、个人建议

只有在并发不高的项目,管理系统中用。如果是面向用户的高并发应用,都不要使用。触发器和存储过程本身难以开发和维护,不能高效移植。触发器完全可以用事务替代。存储过程可以用后端脚本替代。

​

六、热文推荐

  • 在Ubuntu上使用IDEA搞开发是种什么体验?没想到竟是…最后有惊喜!
  • 520夜我花了288大洋就得到了小师妹青睐,原因竟是…一定要看到最后!
  • 你一定没用过的代码生成工具,好不好用你们说了算
  • 如何实现Springboot项目保存本地系统日志文件,超详细,你值得拥有!

如果觉得这篇文章对你有所帮助,还请不忘在文章的左下角,

直接pia的一下点亮它 up up up!!!

若是我,不用犹豫直接进我的收藏夹吃灰去吧!不管以后用不用的上,先吃上灰再说,哈哈哈哈哈嗝~~

​

❤如果文章对您有所帮助,就请在文章末尾的左下角把大拇指点亮吧!(#^.^#);

❤如果喜欢bug菌分享的文章,就请给bug菌点个关注吧!(๑′ᴗ‵๑)づ╭❤~;

❤对文章有任何问题欢迎小伙伴们下方留言或者入群探讨【群号:708072830】;

❤鉴于个人经验有限,所有观点及技术研点,如有异议,请直接回复参与讨论(请勿发表攻击言论,谢谢);

❤版权声明:本文为博主原创文章,转载请附上原文出处链接和本文声明,版权所有,盗版必究!(*^▽^*).

​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

iOS App 启动优化 前言 启动流程: 阶段优化项

发表于 2021-11-16

简介: 作为程序猿来说,“性能优化”是我们都很熟悉的词,也是我们需要不断努⼒以及持续进⾏的事情;其实优化是⼀个很⼤的课题,因为细分来说的话有⼤⼤⼩⼩⼗⼏种优化⽅向 ,但是切忌在实际开发过程中不能盲⽬的 为了优化⽽优化,这样有时可能会造成适得其反的负效果,需要我们根据实际场景以及业务需求进⾏合理优 化。接下来进⼊正题,本⽂将会以iOS App的启动优化为展开点进⾏探讨。

前言

作为程序猿来说,“性能优化”是我们都很熟悉的词,也是我们需要不断努⼒以及持续进⾏的事情;其实优化是⼀个很⼤的课题,因为细分来说的话有⼤⼤⼩⼩⼗⼏种优化⽅向 ,但是切忌在实际开发过程中不能盲⽬的 为了优化⽽优化,这样有时可能会造成适得其反的负效果,需要我们根据实际场景以及业务需求进⾏合理优 化。接下来进⼊正题,本⽂将会以iOS App的启动优化为展开点进⾏探讨。

启动流程:

iOS App 的启动我们都知道分为 为pre-main 和 main() 两个阶段,并且在这两个阶段中,系统会进 ⾏⼀系列的加载操作,过程如下:

pre-main阶段

  1. 加载应⽤的可执⾏⽂件
  1. 加载dyld动态连接器
  1. dyld递归加载应⽤所有依赖的动态链接库dylib

main()阶段

  1. dyld调⽤ main()
  1. 调⽤UIApplicationMain()
  1. 调⽤applicationWillFinishLaunching
  1. 调⽤didFinishLaunchingWithOptions

阶段优化项

1、pre-main阶段

针对 pre-main 阶段做优化时,我们需要先详细了解其加载过程,这个可以在2016年WWDC 的 Optimizing App Startup Time 中详细了解到, 相关材料

1.1 Load dylibs

这⼀阶段dyld会分析应⽤依赖的 dylib (xcode7以后.dylib已改为名.tbd),找到其 mach-o ⽂件,打开和读取这些⽂件并验证其有效性,接着会找到代码签名注册到内核,最后对 dylib 的每⼀个 segment 调⽤ mmap()。不过这⾥的 dylib ⼤部分都是系统库,不需要我们去做额外的优化。

优化结论:

1、尽量不使⽤内嵌的dylib,从⽽避免增加 Load dylibs开销

2、合并已有的dylib和使⽤静态库(static archives),减少dylib的使⽤个数

3、懒加载dylib,但是要注意dlopen()可能造成⼀些问题,且实际上懒加载做的⼯作会更多

1.2 Rebase/Bind

在dylib的加载过程中,系统为了安全考虑,引⼊了ASLR (Address Space Layout Randomization)技术和 代码签名。由于ASLR的存在,镜像(Image,包括可执⾏⽂件、 dylib和bundle)会在随机的地址上加载,和 之前指针指向的地址(preferred_address)会有⼀个偏差(slide), dyld需要修正这个偏差,来指向正确的 地址。 Rebase在前, Bind在后, Rebase做的是将镜像读⼊内存,修正镜像内部的指针,性能消耗主要在 IO。 Bind做的是查询符号表,设置指向镜像外部的指针,性能消耗主要在CPU计算。

优化结论:

在此过程中,我们需要注意的是尽量减少指针数量,⽐如:

  1. 减少ObjC类(class)、⽅法(selector)、分类(category)的数量
  1. 减少C++虚函数的的数量(创建虚函数表有开销)
  1. 使⽤ Swift struct (内部做了优化,符号数量更少)

1.3 Objc setup

⼤部分ObjC初始化⼯作已经在Rebase/Bind阶段做完了,这⼀步dyld会注册所有声明过的ObjC类,将分类插 ⼊到类的⽅法列表⾥,再检查每个selector的唯⼀性。

在这⼀步倒没什么优化可做的, Rebase/Bind阶段优化好了,这⼀步的耗时也会减少。

1.4 Initializers

在这⼀阶段, dyld开始运⾏程序的初始化函数,调⽤每个Objc类和分类的+load⽅法,调⽤C/C++ 中的构造器 函数(⽤attribute((constructor))修饰的函数),和创建⾮基本类型的C++静态全局变量。 Initializers阶段执⾏ 完后, dyld开始调⽤main()函数。

优化结论:

  1. 少在类的+load⽅法⾥做事情,尽量把这些事情推迟到+initiailize
  1. 减少构造器函数个数,在构造器函数⾥少做些事情
  1. 减少构造器函数个数,在构造器函数⾥少做些事情

2、main()阶段

在这⼀阶段⾥,主要优化重点放在 SDK初始化、业务⼯具注册、整体

didFinishLaunchingWithOptions ⽅法中,因为我们的⼀些第三⽅ app ⻛格配置、启动引导⻚显示状态逻辑、版本更新逻辑等等基本⽅都会在这⾥进⾏,如果这部分逻辑没有做好优化梳理,随着业务不断拓展,臃肿的业务逻辑会直接导致启动时 间加⻓。

在满⾜业务需求的前提下,尽量减少 didFinishLaunchingWithOptions ⽅法在主线程中的事件处理逻辑, ⽐如:

  1. 根据实际业务状况,梳理各个⼆⽅/三⽅库,找到可以延迟加载的库,做延迟加载处理,⽐如放到⾸⻚控制器 的viewDidAppear⽅法⾥。
  1. 梳理业务逻辑,把可以延迟执⾏的逻辑,做延迟执⾏处理。⽐如检查新版本、注册推送通知等逻辑
  1. 避免进⾏⼀些复杂/多余的计算逻辑,这类逻辑尽量进⾏异步延迟处理
  1. 避免在⾸⻚控制器的viewDidLoad和viewWillAppear做太多容易阻塞主线程的事情,这2个⽅法执⾏完, ⾸⻚控制器才能显示

场景补充:

另外,在我们实际开发过程中,很多项⽬的⾸⻚控制器都会有⼀些后台可配、较为丰富的结构或者推荐数据 进⾏展示,⽽且我们的⾸⻚展示速度通常也会被纳⼊启动优化的⼀部分,其实对于这种类型的优化,如果我 们还只是⽤传统的 api -> data -> UI ⽅式进⾏的话,就很难有明显的改善空间,因为⽤户的⽹络状态 并不是可控项,如果不做其他处理的话,那在很多场景下对⽤户来说,即使我们放上⼀些占位图,展示的样式也是很不友好的,毕竟⾸⻚控制器对⽤户的第⼀视觉冲击影响还是⽐较⼤的。

对于这种场景下的优化来说,⼀般我们可以采取 Local + Network + Update 的⽅式在⼀定程度上优化 ⾸⻚加载速度: 即:

1、 app更新过程中,⾸先进⾏本地内嵌处理逻辑,内嵌⾸⻚数据结构( localDataBase)、内嵌⾸⻚样式所需 资源( localStorage)

2、在安装启动之后,对本地与线上数据更新记录进⾏对⽐,检测是否需要更新本地内嵌数据结构

3、检测到有需要更新的数据时,才会对指定结构进⾏静默更新,并且同步更新本地数据结构

这样做的好处是:

1、⾸⻚数据直接从本地加载,减少⽹络数据等待时间

2、仅检测数据key值变化,⼩数据量对⽐定向更新结构,减少api数据交互频次及数据包体积

3、能够保证⾸⻚对于⽤户来说会⼀直处于⼀个友好的展示状态

当然这种也并不是唯⼀的应对⽅式,⽽且也并⾮对所有场景都适⽤,只是提供⼀种思路⽽已,还是需要根据 项⽬的实际场景选择适合的优化⽅案。

统计时⻓

另外如果在开发过程中,我们想直观的查看 app 启动期间,各阶段的耗时情况,也可以在Xcode,的 edit scheme 设置添加 DYLD_PRINT_STATISTICS 为1 ,打印启动时⻓,例如

优化前启动时⻓:

优化后启动时⻓:

当然,这些log我们仅仅只能在开发调试阶段查看打印,那么在实际项⽬中,我们需要对线上项⽬的启动数据 进⾏监控,以便及时的定位和优化那些影响 app 启动时⻓的环节,这时我们应该怎样更好的处理呢?

当然我们可以通过服务器埋点上报的⽅式⾃⾏统计分析,不过这样⼀来会发现我们的统计成本就会⼤⼤增 加,⽽且结果分析也会变得不那么灵活。所以这⾥推荐⼀种简单的监控⽅式,那就是友盟的 U-APM 应能性 能监控SDK ,只需要我们进⾏简单的pod集成之后,便可根据我们的实际需要进⾏⼿动或者⾃动监控启动数 据,详情可以参考 U-APM, 并且为了⽅便我们对数据进⾏分析,友盟后台已经根据这些数据帮我们绘制出 了对应的分布图,我们可以⼀⽬了然的得出启动耗时分布、启动类型占⽐等等,如图:

除此之外,我们还可以通过SDK进⾏崩溃分析、 ANR分析、监控告警、卡顿分析、内存分析等等诸多功能, 有了 U-APM 这个监控平台,其实在实际开发过程中很⼤程度的提升了我们对线上 app 的优化分析效率。

当然本⽂的介绍也只是⽐较浅显的优化项,仅供参考以及思路引导,优化之路任重⽽道远,还需要我们不断 的去探索、发现、提⾼。不过最后还是要提醒⼀句:在实际项⽬开发过程中,不要为了优化⽽优化,要根据 项⽬情况有针对性的进⾏优化。

参考:

探秘 Mach-O ⽂件

iOS底层 - 从头梳理 dyld 加载流程

iOSapp启动 - dyld加载App流程

wwdc2016optimizingappstartuptime.pdf

作者:武玉宝

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JDK内置帮助JVM故障定位与处理的几个小工具 前言 工具说

发表于 2021-11-16

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

前言

JVM进行故障定位主要是对系统运行时的一些数据进行处理及分析,如堆栈信息、线程快照等。

JDK自带了一些工具可以帮助开发人员或才运维人员进行故障定位。当然了在开发过程中可能用到的相对较少,一般是在线上环境时进行故障定位或才优化时需要相关数据分析。

这些工具包括虚拟机进程状况工具(jps)、虚拟机统计信息监视工具(jstat)、java配置信息工具(jinfo)、java内存映像工具(jmap)、虚拟机堆转储快照分析工具(jhat)、java堆栈跟踪工具(jstack)和可视化的多合一故障处理工具(jvisualvm) 。

ps:这些工具在安装 JDK的时候都在JDK的bin目录下,就和java、javac一样,如果配置了环境变量直接调用jps、jvisualvm我等命令就可以看到,当然了在一些线上环境权限设置比较严格的或者没有配置环境变量的,可以直接进行bin目录内执行。另外,本文基于JDK1.7测试的。

接下来对这些工具的简单使用及一些场景进行说明,本文只列出个人常用的一些用法及参数说明,详细的了解推荐阅读 《深入理解Java虚拟-JVM高级特性与最佳实践》 一书的第4章:《虚拟机性能监控与故障处理工具》

工具说明

  1. jps:虚拟机进程状况工具

简单来说,它的作用和ps命令一样,但是只显示java进程。


执行jps就会把当前系统的所有java进程列表出来,左侧一列是进程号,右侧是虚拟机执行主类名。不过建议使用jps -l,输出主类全名或执行jar包的路径,这样可以更容易区分是哪个进程。


其它还有-v(输出虚拟机启动时JVM参数),-m(启动时传递给主类的参数)等。


我个人其实使用的jps -l比较多,就是查看下进程信息。
  1. jstat:虚拟机统计信息监视工具

这个主要是对虚拟机运行状态监控的工具,笔者平常就用它查下GC信息。


命令基本用法:jstat 参数 进程ID 查询间隔 查询次数,如jstat -gc 11036 1000 5,就是查询进程11306的java堆的GC状况,每隔1秒(1000ms)查询一次,共查询5次,具体效果如下图:

我首先用jps -l查询了本机运行的java进程信息,然后查看了11036进程的GC信息,对于GC信息的各个字段说明如下:

S0C/S1C:这是两个Survivor(Survivor0和Survivor1区的总内存,这两个区属性新生代的一部分)

S0U/S1U:就是两个Survivor的已经使用内存

EC:就是新生代Eden区的总内存,EU:则是新生代Eden区的已使用内存(C/U这两个简写需要望文生义)

OC:老年代总内存,OU:老年代已用内存

PC:持久代总内存,PU:持久代已用内存(JDK1.8其实不用太关注持久代,毕竟移除了,其实这个区实际上可以动态增长了)

YGC:新生代的GC总次数,YGCT:新生代的GC总时间

FGC:整个堆内存进行GC的总次数,FGCT:Full GC花费的总时间

GCT:整个GC过程花费的总时间

  1. jinfo:Java配置信息工具

这个工具主要用来查看虚拟机参数,和环境变量的值,这里重点提下环境变量的值:


使用jifno -sysprops 进程ID,即可查看该进程使用System.getProperties()获取到的所有内容。包括运行过程中代码System.setProperty(k,v)等设置的所有的内容。有某些特殊情况定位数据很有用了,用到了就知道了。

ps:其它用法参考下文提到的参考资料查看即可,本文只是对某些内容作一下重点强调和补充

  1. jmap:Java内存映像工具

这个工具用于生成堆转储快照。本方只对基本用法进行举例说明:


用法:jamp 参数 进程ID,想要堆转储快照参考这个示例(在JDK1.7测试):jmap -dump:format=b,file=test.hprof 11036,会在用户目录下生成一个进程11036的dump快照文件test.hprof,具体怎么分析这个文件,且看下文。
  1. jhat:虚拟机堆转储快照分析工具

这个工具就是对第4节的文件进行可视分析,使用示例:jhat test.hprof,会在本地启动一个服务器,默认端口7000,可以通过浏览器访问<http://localhost:7000> 就可以查看了。启动前保证端口7000没有被其它进程占用。


建议不用这个工具,如果条件允许,请使用下文说明的jvisualvm进行分析。
  1. jstack:Java堆栈跟踪工具

这个工具,可以生成本地线程快照,用法:jstack 参数 进程ID,示例:jstack 11036(参数可选).


ps:进程如果太多,建议保存到一个文件,用一个功能便捷的文本工具根据自己想要的信息进行统计分析。也可以生成一个快照导入jvisualvm查看
  1. jvisualvm:可视化的多合一故障处理工具

这个用起来就很爽,直观的界面,强大监控功能与数据分析。直接输入jvisualvm命令即可打开,可以监控本地也可以远程连接。至于它可以做哪些希望可以查阅本文最后的参考资料,或者自己打开看一下。


主要说下用它的比较尴尬的地方:1、线上环境可能是不支持的,一般是堆转储快照或者线程快照拉到下来在线下的机器上打开jvisualvm进行分析;2、堆文件过大的时候,分析起来对机器的配置要求真的非常高,要不分析个数据每点一下都要等好久,才能出来结果;3、掌握OQL脚本语言会定位起来更轻松一点,增加了使用成本。反正之前笔者公司有个同事后两条条件都具备,10个G的dump被人家用来分析真是6的不要不要的。


所以这个工具的使用就看个人的道行深浅了。如果是基本的看内存、CPU信息什么的,就没什么要求了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Guava Cache实战—从场景使用到原理分析

发表于 2021-11-16

摘要:本文先介绍为什么使用 Guava Cache 缓存;然后讲解 Guava 缓存使用方法、清理方法及使用过程中踩过的坑;接着讲解其底层数据结构,分析其性能优异的原因;最后讲解在使用本地缓存时可以优化的方向。

为什么使用本地缓存

在多线程高并发场景中往往是离不开 cache 的,需要根据不同的应用场景来需要选择不同的 cache,比如分布式缓存如 Redis、memcached,还有本地(进程内)缓存如 ehcache、Guava Cache。缓存相比IO操作,速度快,效率高;Guava 相比 Redis 来说,应用和cache是在同一个进程内部,请求缓存非常迅速,没有过多的网络开销。redis 的好处是自身就是一个独立的应用,多个应用可以共享缓存。我们应该根据数据类型、业务场景来判断应该使用哪种类型的缓存,以达到减少计算量,提高响应速度的目的。
在这里插入图片描述

本地缓存适用场景(需都满足)

  • 愿意消耗一些内存空间来提升速度
  • 预料到某些键会被多次查询
  • 缓存中存放的数据总量不会超出内存容量
  • 要更快的响应,缓存不需要网络 io(集中式缓存需要额外网络 io)

如何使用 Guava 缓存

Guava Cache 是 Google 开源的 Java 重用工具集库 Guava 里的一款缓存工具,下面介绍接入 guava 缓存的步骤及使用过程中踩过的坑

Guava Cache 接入demo

  • 1、导入 Maven 引用
1
2
3
4
5
XML复制代码<dependency>  
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
  • 2、Cache 初始化,使用了 Builder 设计模式,可自行设置各类参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码private final LoadingCache<Long, TestDemo> demo = CacheBuilder.newBuilder()  
//设置 cache 的初始大小为10,要合理设置该值
.initialCapacity(10)
//设置并发数为5,即同一时间最多只能有5个线程往 cache 执行写入操作
.concurrencyLevel(5)
//最大 key 个数
.maximumSize(100)
//移除监听器
.removalListener(removalListener)
//设置 cache 中的数据在写入之后的存活时间为10秒
.expireAfterWrite(10, TimeUnit.SECONDS)
//构建 cache 实例
.build(new CacheLoader<Long, TestDemo>() {
@Override
public TestDemo load(Long id) throws Exception {
// 读取 db 数据
}
});

RemovalListener<String, String> removalListener = new RemovalListener<String, String>() {
public void onRemoval(RemovalNotification<id, TestDemo> removal) {
System.out.println("[" + removal.getKey() + ":" + removal.getValue() + removal.getCause() + "] is evicted!");
}
};
  • 3、 如果缓存加载方法时出现了异常,那么下一次是缓存异常还是正常数据?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码public class TestDemoImpl implements TestDemo {
int time;
private final LoadingCache<Long, TestDemo> demo;
this.demo1 = CacheBuilder.newBuilder()
.expireAfterWrite(duration, TimeUnit.HOURS)
.build(new CacheLoader<Long, TestDemo>() {
@Override
public TestDemo load(Long id) throws Exception {
// 查询 db
if (time++==0) {
throw new RunTimeException();
}
return result;
}
});
}
public static void main(String[] args) {
try {
try {
TestDemo testDemo = demoCache.getUnchecked(21L);
System.out.println("第一次查询:" + testDemo);
} catch (Exception e) {
}
TestDemo testDemo1 = testDemo.getUnchecked(21L);
System.out.println("第二次查询:" + testDemo1);
} catch (Exception e) {
e.printStackTrace();
}
}
  • 结论:第二次返回正常数据

guava 使用踩坑

  • 1、使用 guava cache 时避免使用 weakkeys。weakkeys 对 key 的命中规则是 ==,如果使用非基本类型,会因为 key 判断不相等导致缓存无法命中。
  • 2、仅仅需缓存元数据本身,不要缓存其关系,否则造成笛卡尔积。如缓存的数据由A,B,C三张表的维度组成,缓存关系会导致A X B X C的数据量,如果缓存元数据,则缓存的数据量仅为 A+B+C;
  • 3、使用缓存前必须预估缓存的数据大小,并设置缓存的数量或大小。如果不设置过期方式的话,也不设置大小,缓存数据将无法回收,会引起 OOM

如何清理 Guava 缓存

在日常开发过程中,最常见的问题是,如何清理 Guava 缓存,下面介绍两种清理方式

手动清理

通过 rest 后门等手动调用

1
2
3
4
5
java复制代码// 获取最新 db 数据更新缓存
testDemoCache.refresh(key);

// 清理 guava cache 缓存
estDemoCache.invalidateAll();

自动清理缓存

手动清理缓存,人力成本较高,现在有一种方案是,在新增/修改数据后,通过 MQ 广播来实现所有机器自动清理缓存。或通过 canal 等 binlog 中间件监听 db 变更来清理缓存。

Guava cache 原理解析

Guava cache 继承了 ConcurrentHashMap 的思路,使用多个 segments 方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求。下面介绍它的底层实现,分析其性能优异的原因

几个重要的组件

1、CacheBuilder 缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。采用 Builder 设计模式提供了设置好各种参数的缓存对象。
2、LocalCache 数据结构。缓存核心类 LocalCache 数据结构与 ConcurrentHashMap 很相似,由多个 segment 组成,且各 segment 相对独立,互不影响,所以能支持并行操作,每个 segment 由一个 table 和若干队列组成。缓存数据存储在 table 中,其类型为AtomicReferenceArray。

在这里插入图片描述

上图为 Guava cache 底层的数据结构图,下表对其结构做了说明

序号 数据结构 特点 详解
1 Segment<K, V>[] segments Segment 继承于ReetrantLock,减小锁粒度,提高并发效率
2 AtomicReferenceArray<ReferenceEntry<K, V>> table 类似于HasmMap 中的table 一样,相当于 entry 的容器
3 ReferenceEntry<K, V> referenceEntry 基于引用的Entry,其实现类有弱引用 Entry,强引用 Entry 等 Cache 由多个 Segment 组成,而每个 Segment 包含一个ReferenceEntry 数组,每个 ReferenceEntry 数组项都是一条 ReferenceEntry 链,且一个 ReferenceEntry 包含key、hash、valueReference、next 字段。除了在ReferenceEntry 数组项中组成的链,在一个 Segment中,所有 ReferenceEntry 还组成 access 链(accessQueue)和 write 链(writeQueue)。ReferenceEntry 可以是强引用类型的 key,也可以WeakReference 类型的 key,为了减少内存使用量,还可以根据是否配置了 expireAfterWrite、expireAfterAccess、maximumSize 来决定是否需要 write 链和 access 链确定要创建的具体 Reference:StrongEntry、StrongWriteEntry、StrongAccessEntry、StrongWriteAccessEntry等
4 ReferenceQueue keyReferenceQueue 已经被 GC,需要内部清理的键引用队列
5 ReferenceQueue valueReferenceQueue 已经被 GC,需要内部清理的值引用队列 因为 Cache 支持强引用的 Value、SoftReference Value 以及 WeakReference Value,因而它对应三个实现类:StrongValueReference、SoftValueReference、WeakValueReference。为了支持动态加载机制,它还有一个 LoadingValueReference,在需要动态加载一个 key的值时,先把该值封装在 LoadingValueReference 中,以表达该 key 对应的值已经在加载了,如果其他线程也要查询该 key 对应的值,就能得到该引用,并且等待改值加载完成,从而保证该值只被加载一次,在该值加载完成后,将 LoadingValueReference 替换成其他 ValueReference类型。ValueReference 对象中会保留对 ReferenceEntry 的引用,这是因为在 Value 因为 WeakReference、SoftReference 被回收时,需要使用其 key 将对应的项从Segment 的 table 中移除
6 Queue<ReferenceEntry<K, V>> recencyQueue 记录升级可访问列表清单时的entries ,当segment 上达到临界值或发生写操作时该队列会被清空
7 Queue<ReferenceEntry<K, V>> writeQueue 按照写入时间进行排序的元素队列,写入一个元素时会把它加入到队列尾部 为了实现最近最少使用算法,Guava Cache 在 Segment 中添加了两条链:write 链(writeQueue)和 access 链(accessQueue),这两条链都是一个双向链表,通过ReferenceEntry 中的 previousInWriteQueue、nextInWriteQueue 和 previousInAccessQueue、nextInAccessQueue 链接而成,但是以 Queue 的形式表达。WriteQueue 和 AccessQueue 都是自定义了 offer、add(直接调用 offer)、remove、poll 等操作的逻辑,对offer(add)操作,如果是新加的节点,则直接加入到该链的结尾,如果是已存在的节点,则将该节点链接的链尾;对 remove 操作,直接从该链中移除该节点;对 poll操作,将头节点的下一个节点移除,并返回
8 Queue<ReferenceEntry<K, V>> accessQueue 按照访问时间进行排序的元素队列,访问(包括写入)一个元素时会把它加入到队列尾部

guava常用接口

1
2
3
4
5
java复制代码/** 
* 该接口的实现被认为是线程安全的,即可在多线程中调用
* 通过被定义单例使用
*/
public interface Cache<K, V> {
1
2
3
4
java复制代码/** 
* 通过key获取缓存中的value,若不存在直接返回null
*/
V getIfPresent(Object key);

在这里插入图片描述

上图为方法 getIfPresent 执行流程图

1
2
3
4
5
6
java复制代码/** 
* 通过 key 获取缓存中的 value,若不存在就通过 valueLoader 来加载该 value
* 整个过程为 "if cached, return; otherwise create, cache and return"
* 注意 valueLoader 要么返回非 null 值,要么抛出异常,绝对不能返回 null
*/
V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;

在这里插入图片描述

上图为 get 方法执行流程图

1
2
3
4
java复制代码/** 
* 添加缓存,若key存在,就覆盖旧值
*/
void put(K key, V value);

在这里插入图片描述

上图为 put 方法执行流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码/** 
* 删除该key关联的缓存
*/
void invalidate(Object key);

/**
* 删除所有缓存
*/
void invalidateAll();

/**
* 执行一些维护操作,包括清理缓存
*/
void cleanUp();
}

在这里插入图片描述

上图为移除缓存的流程图

缓存回收

Guava Cache 提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。 基于容量的方式内部实现采用 LRU 算法,基于引用回收很好的利用了 Java 虚拟机的垃圾回收机制。

1、基于容量的回收(size-based eviction)

如果要规定缓存项的数目不超过固定值,只需使用 CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。在缓存项的数目达到限定值之前,缓存就可能进行回收操作,通常来说,这种情况发生在缓存项的数目逼近限定值时。

2、定时回收(Timed Eviction)

CacheBuilder 提供两种定时回收的方法:
expireAfterAccess(long, TimeUnit) :缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于容量回收一样。
expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。

定时回收周期性地在写操作中执行,偶尔在读操作中执行。

3、基于引用的回收(Reference-based Eviction)

通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache 可以把缓存设置为允许垃圾回收
CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(= =),使用弱引用键的缓存用= = 而不是 equals 比较键。
CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(= =),使用弱引用值的缓存用= =而不是 equals 比较值。
CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是 equals 比较值。

4、显式清除

任何时候,你都可以显式地清除缓存项,而不是等到它被回收:
个别清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有缓存项:Cache.invalidateAll()

5、移除监听器

通过 CacheBuilder.removalListener (RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener 会获取移除通知 [RemovalNotification],其中包含移除原因 [RemovalCause]、键和值

6、统计
CacheBuilder.recordStats():用来开启 Guava Cache 的统计功能。统计打开后,Cache.stats() 方法会返回 CacheStats 对象以提供如下统计信息:

  • hitRate():缓存命中率;
  • averageLoadPenalty():加载新值的平均时间,单位为纳秒;
  • evictionCount():缓存项被回收的总数,不包括显式清除。

此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。

清理什么时候发生?

使用 CacheBuilder 构建的缓存不会”自动”执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做(如果写操作实在太少的话)。
这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样 CacheBuilder 就不可用了。
相反,我们把选择权交到你手里。如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的 缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用 Cache.cleanUp()。ScheduledExecutorService 可以帮助你很好地实现这样的定时调度。

优化方向

  • 1、在使用便利性优化,SpringBoot 集成 Guava Cache 实现本地缓存
  • 2、在性能上优化,使用 Caffeine 缓存替代 Guava 缓存

Caffeine 是基于 Java8 实现的新一代缓存工具,缓存性能接近理论最优。可以看作是 Guava Cache 的增强版,API 上两者类似,主要区别体现在内存淘汰机制;因为在现有算法的局限性下,会导致缓存数据的命中率或多或少的受损,而命中率又是缓存的重要指标 。Caffeine 使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率 。

Caffeine 实现机制

  • TinyLFU 维护了近期访问记录的频率信息,作为一个过滤器,当新记录来时,只有满足 TinyLFU 要求的记录才可以被插入缓存。它需要解决两个挑战:一个是如何避免维护频率信息的高开销;另一个是如何反应随时间变化的访问模式。
  • 在 Count-Min Sketch 中,如果你的缓存大小是100,他会生成一个 long 数组大小是和100最接近的2的幂的数,也就是128。而这个数组将会记录我们的访问频率。在 caffeine 中规定频率最大为15,15的二进制位1111,总共是4位,而 Long 型是64位。而 Caffeine 用了四种 hash 算法,每个 Long 型被分为四段,每段里面保存的是四个算法的频率。这样做的好处是可以进一步减少 Hash 冲突,原先128大小的 hash,就变成了128X4。

算法细节如下图所示:
在这里插入图片描述

1、读写性能

  • 在 guava cache 中其读写操作中夹杂着过期时间的处理,也就是你在一次 Put 操作中有可能还会做淘汰操作,所以其读写性能会受到一定影响,而在 caffeine,对这些事件的操作是通过异步操作,他将事件提交至队列,这里的队列的数据结构是 RingBuffer,然后会通过默认的 ForkJoinPool.commonPool(),或者自己配置线程池,进行取队列操作,然后在进行后续的淘汰,过期操作。读写也是有不同的队列,在 caffeine 中认为快取读比写多很多,所以对于写操作是所有执行共享一个 Ringbuffer。

2、数据淘汰策略

  • 在 caffeine 所有的数据都在 ConcurrentHashMap 中,这个和 guava cache 不同,guava cache 是自己实现了个类似ConcurrentHashMap 的结构。在 caffeine 中有三个记录引用的 LRU 队列:
  • Eden 队列:在 caffeine 中规定只能为缓存容量的1%,如果 size =100,那这个队列的有效大小就等于1。这个队列中记录的是新到的数据,防止突发流量由于之前没有访问频率,而导致被淘汰。新建最开始其实是没有访问频率的,防止上线之后被其他缓存淘汰出去,而加入这个区域。伊甸区,最舒服最安逸的区域,在这里很难被其他数据淘汰。
  • Probation 队列:叫做缓刑队列,在这个队列就代表你的数据相对比较冷,马上就要被淘汰了。这个有效大小为 size 减去 eden 减去protected。
  • Protected 队列:在这个队列中,暂时不会被淘汰,如果 Probation 队列没有数据了或者 Protected 数据满了,你也将会被面临淘汰的尴尬局面。当然想要变成这个队列,需要把 Probation 访问一次之后,就会提升为 Protected 队列。这个有效大小为(size 减去 eden) X 80% 如果 size =100,就会是79。

在这里插入图片描述

1、所有的新数据都会进入 Eden。
2、Eden 满了,淘汰进入 Probation。
3、如果在 Probation 中访问了其中某个数据,则这个数据升级为 Protected。
4、如果 Protected 满了又会继续降级为 Probation。
5、对于发生数据淘汰的时候,会从 Probation 中进行淘汰。会把这个队列中的数据队头称为受害者,这个队头肯定是最早进入的,按照 LRU 队列的算法的话那他其实他就应该被淘汰,但是在这里只能叫他受害者,这个队列是缓刑队列,代表马上要给他行刑了。这里会取出队尾叫候选者,也叫攻击者。这里受害者会和攻击者PK决出应该被淘汰的。

通过我们的Count-Min Sketch中的记录的频率数据有以下几个判断:

1、如果攻击者大于受害者,那么受害者就直接被淘汰。
2、如果攻击者<=5,那么直接淘汰攻击者,作者认为设置一个预热的门槛会让整体命中率更高。
3、其他情况,随机淘汰。

3、其他优化
过期策略:在 Caffeine 中有个 scheduleDrainBuffers 方法,用来进行我们的过期任务的调度,在读写之后都会对其进行调用:
更新策略:建立一个 CacheLoader 来进行重新整理,这里是同步进行的,可以通过 buildAsync 方法进行非同步构建,自动刷新只存在读操作之后;

总结

Guava Cache 基于 ConcurrentHashMap 的优秀设计借鉴,在高并发场景支持和线程安全上都有相应的改进策略,使用 Reference引用命令,提升高并发下的数据访问速度并保持了 GC 的可回收,有效节省空间;同时,write 链和 access 链的设计,能更灵活、高效的实现多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等;编程式的 build 生成器管理,让使用者有更多的自由度,能够根据不同场景设置合适的模式。

GuavaCache 的实现代码中没有启动任何线程,Cache 中的所有维护操作,包括清除缓存、写入缓存等,都需要外部调用来实现。这在需要低延迟服务场景中使用时,需要关注,可能会在某个调用的响应时间突然变大。GuavaCache 毕竟是一款面向本地缓存的,轻量级的Cache,适合缓存少量数据。如果你想缓存上千万数据,可以为每个 key 设置不同的存活时间,并且高性能,那并不适合使用GuavaCache。

参考链接

  • tech.meituan.com/2017/03/17/…
  • www.jianshu.com/p/38bd5f1cf…
  • www.imooc.com/article/det…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

关于Netty核心组件浅薄的理解 Netty的核心组件

发表于 2021-11-16

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

我一开始学习Netty和了解Netty也是很迷茫,因为它里面很复杂,那个时候我一行代码看不懂,然后我阅读了很多关于Netty的书籍,我结合书中的理论加实际的源码部分,使我对Netty组件有了一个基本的了解,理解多少,我就写多少,以帮助后续想了解Netty的人在了解Netty的路上更高效。

本文介绍一些Netty的核心组件

Netty的核心组件

Channel

Channel可以看成是java Nio的一个基本组成部分,这里你可以把它当成是入站或者出站的数据载体,源码对Channel的解释如下。

1
2
css复制代码 A nexus to a network socket or a component which is capable of I/O
* operations such as read, write, connect, and bind.

使用Channel给我们提供了读、写、连接和绑定操作。

回调

Netty在内部使用了回调来处理信息,就是说:当一个方法被调用或者一个连接被创立时,Netty会使用回调来告知我们相关信息。

Netty并没有使用JDK提供的interface java.util.concurrent.Future,而是自己进行了内部实现,ChannelFuture。

ChannelFuture提供了几种能够注册一个或者多个ChannelFutureListener实例的额外的方法。

监听器的回调方法operationComplete(),将会在对应的操作完成时被调用。

每个Netty的出站I/O操作都将返回一个ChannelFuture,正如我们前面所提到过的一样,Netty完全是异步和事件驱动的。

源码里演示了一个ChannelFuture的例子:这里bind方法不会阻塞。
image.png

事件和ChannelHandler

Netty本身是基于事件驱动的,这意味着它能基于已经发生的事件做出不同的操作动作,这些动作可能是:

  • 记录日志;
  • 数据转换;
  • 流控制;
  • 应用程序逻辑。

Netty里面可能由入站数据或者状态更改触发的事件包括:

  • 连接已被激活或者连接失活;
  • 数据读取;
  • 用户事件;
  • 错误事件。

有入站还可能有出站事件,出站事件是未来某个事件触发后形成的结果,包括:

  • 打开或者关闭到远程节点的连接;
  • 将数据写到或者冲刷到套接字。

每个事件都可以被分发给ChannelHandler类中的某个用户实现的方法。
每个事件都会经过很多的ChannelHandler进行处理,这些Handler都封装在这个pipeline里面。

Selector和EventLoop

Selector是Netty里面对所有事件进行抽象的集合,在我们的服务启动的时候创建,然后是需要把我们选择的Channel注册到我们选择的这个EventLoop的Selector上。

每个EventLoop可以处理的事件包括:

  • 注册感兴趣的事件;
  • 将事件派发 给ChannelHandler;
  • 安排进一步的动作。

EventLoop本身只由一个线程驱动,其承担着处理一个Channel的所有事件的任务,这种强大而又简单的设计使我再一次感叹道Netty设计的魅力。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

小码农教你双链表 双链表

发表于 2021-11-16

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」

双链表

双链表结构图

image-20211028225455233

双链表节点

1
2
3
4
5
6
7
8
c复制代码typedef int LTDataType;  //C++中的双链表是用list表示的

typedef struct ListNode
{
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;

双链表初始化函数ListInit

1
2
3
4
5
6
7
8
9
c复制代码//双链表初始化函数
LTNode* ListNodeInit()
{
//创建一个双链表哨兵位头结点 不存储有效数据 循环
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
phead->next = phead;
phead->prev = phead;
return phead;
}

image-20211028230625085

双链表尾插函数ListPushBack

image-20211029001350843

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码//双链表尾插函数
void ListNodePushBack(LTNode* phead, LTDataType x)
{
assert(phead);//实际上phead永远也不可能是NULL
LTNode* tail = phead->prev;
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->data = x;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}

双链表打印函数ListPrint

image-20211029070928255

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码//双链表打印函数
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}

双链表尾删函数ListPopBack

image-20211029072838324

1
2
3
4
5
6
7
8
9
10
11
c复制代码//双链表尾删函数
void ListPopBack(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* tail = phead->prev;
LTNode* cur = phead->prev;
tail = tail->prev;
tail->next = phead;
phead->prev = tail;
free(cur);
}

双链表头插函数ListPushFront

image-20211029080341212

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码//双链表头插函数
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* next = phead->next;//在next和phead中间插一个节点
/*LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->data = x;*/
LTNode* newnode = BuyListNode(x);
newnode->next = next;
next->prev = newnode;
phead->next = newnode;
}

既然我们头插尾插都遇到了添加节点,所以我们把添加节点的部分抽离出来重新封装一下

获得双链表节点函数BuyListNode

image-20211029191337400

1
2
3
4
5
6
7
8
9
c复制代码//获得双链表节点函数
LTNode* BuyListNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}

双链表头删函数ListPopFront

image-20211029193358759

1
2
3
4
5
6
7
8
9
c复制代码//双链表头删函数
void ListPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* next = phead->next;
phead->next = next->next;
next->next->prev = phead;
free(next);
}

双链表查找函数ListFind

这个一般是和插入,删除配合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码//双链表查找函数
LTNode* ListFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}

双链表插入函数ListInsert(pos之前插入因为c++中就是之前插入的)

image-20211029204212625

1
2
3
4
5
6
7
8
9
10
11
12
c复制代码//双链表插入函数
void ListInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);
LTNode* posprev = pos->prev;
newnode->data = x;
pos->prev = newnode;
newnode->next = pos;
newnode->prev = posprev;
posprev->next = newnode;
}

双链表删除函数ListErase(删除pos)

image-20211029205627363

1
2
3
4
5
6
7
8
9
10
c复制代码//双链表删除函数
void ListErase(LTNode* pos)
{
assert(pos && pos->next);
LTNode* posnext = pos->next;
LTNode* posprev = pos->prev;
posnext->prev = posprev;
posprev->next = posnext;
free(pos);
}

双链表销毁函数ListDestroy(实际上我在这里写报错了,前面一个函数有bug,但是找到了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码//双链表销毁函数
void ListDestroy(LTNode* phead)
{
assert(phead);
LTNode* tail = phead->prev;
while (tail != phead)
{
LTNode* tailprev = tail->next;
free(tail);
tail = tailprev;
}
free(phead);
phead = NULL;
}

代码

DoubleList.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
c复制代码#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int LTDataType; //C++中的双链表是用list表示的

typedef struct ListNode
{
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;

//双链表初始化函数
extern LTNode* ListInit();
//双链表销毁函数
extern void ListDestroy(LTNode* phead);
//双链表尾插函数
extern void ListPushBack(LTNode* phead, LTDataType x);
//双链表打印函数
extern void ListPrint(LTNode* phead);
//双链表尾删函数
extern void ListPopBack(LTNode* phead);
//获得双链表节点函数
extern LTNode* BuyListNode(LTDataType x);
//双链表头插函数
extern void ListPushFront(LTNode* phead, LTDataType x);
//双链表头删函数
extern void ListPopFront(LTNode* phead);
//双链表查找函数
extern LTNode* ListFind(LTNode* phead, LTDataType x);
//双链表插入函数
extern void ListInsert(LTNode* pos, LTDataType x);
//双链表删除函数
extern void ListErase(LTNode* pos);

DoubleList.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
c复制代码#define _CRT_SECURE_NO_WARNINGS 1

#include "DoubleList.h"

//双链表初始化函数
LTNode* ListInit()
{
//创建一个双链表哨兵位头结点 不存储有效数据 循环
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
phead->next = phead;
phead->prev = phead;
return phead;
}
//获得双链表节点函数
LTNode* BuyListNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
//双链表尾插函数
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);//实际上phead永远也不可能是NULL
LTNode* tail = phead->prev;
/*LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->data = x;*/
LTNode* newnode = BuyListNode(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
//双链表打印函数
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
//双链表尾删函数
void ListPopBack(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* tail = phead->prev;
LTNode* cur = phead->prev;
tail = tail->prev;
tail->next = phead;
phead->prev = tail;
free(cur);
}

//双链表头插函数
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* next = phead->next;//在next和phead中间插一个节点
/*LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->data = x;*/
LTNode* newnode = BuyListNode(x);
newnode->next = next;
next->prev = newnode;
phead->next = newnode;
}

//双链表头删函数
void ListPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* next = phead->next;
phead->next = next->next;
next->next->prev = phead;
free(next);
}

//双链表查找函数
LTNode* ListFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
//双链表插入函数
void ListInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);
LTNode* posprev = pos->prev;
newnode->data = x;
pos->prev = newnode;
newnode->next = pos;
newnode->prev = posprev;
posprev->next = newnode;
}
//双链表删除函数
void ListErase(LTNode* pos)
{
assert(pos && pos->next);
LTNode* posnext = pos->next;
LTNode* posprev = pos->prev;
posnext->prev = posprev;
posprev->next = posnext;
free(pos);
}
//双链表销毁函数
void ListDestroy(LTNode* phead)
{
assert(phead);
LTNode* tail = phead->prev;
while (tail != phead)
{
LTNode* tailprev = tail->next;
free(tail);
tail = tailprev;
}
free(phead);
phead = NULL;
}

test.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
c复制代码#define _CRT_SECURE_NO_WARNINGS 1
#include "DoubleList.h"
void TestList1()
{
LTNode* plist = ListInit();
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPrint(plist);
ListPopBack(plist);
ListPopBack(plist);
ListPopBack(plist);
ListPopBack(plist);
ListPrint(plist);
ListPushFront(plist, 10);
ListPushFront(plist, 20);
ListPushFront(plist, 30);
ListPrint(plist);
LTNode* p1 = ListFind(plist, 10);
if (p1)
{
ListInsert(p1, 100);
}
ListPrint(plist);
LTNode* p2 = ListFind(plist, 20);
if (p2)
{
ListErase(p2);
}
ListPrint(plist);
ListDestroy(plist);
plist = NULL;
}


int main()
{
TestList1();
return 0;
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

当 Swagger 遇上 Torna,瞬间高大上了!

发表于 2021-11-16

Swagger作为一款非常流行的API文档生成工具,相信很多小伙们都在用!用多了可能会觉得它界面丑、功能弱。今天给大家推荐一款工具Torna,配合Swagger使用可以搭建界面漂亮、功能强大的API文档网站,希望对大家有所帮助!

SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…

Torna简介

Torna是一套企业级接口文档解决方案,可以配合Swagger使用。它具有如下功能:

  • 文档管理:支持接口文档增删改查、接口调试、字典管理及导入导出功能;
  • 权限管理:支持接口文档的权限管理,同时有访客、开发者、管理员三种角色;
  • 双模式:独创的双模式,管理模式可以用来编辑文档内容,浏览模式纯粹查阅文档,界面无其它元素干扰。

Torna项目架构

Torna是一个前后端分离项目,后端使用SpringBoot+MyBatis来实现,前端使用Vue+ElementUI来实现,技术栈非常主流!它不仅可以搭建API文档网站,还是个非常好的学习项目,让我们先来看看它的项目架构。

  • 首先我们需要下载Torna的源码,下载地址:gitee.com/durcframewo…

  • 下载成功后,将代码导入到IDEA中,项目结构如下;

  • 我们再来看下server模块的结构,一个非常标准的SpringBoot项目;

  • 再来看下front模块的结构,一个非常标准的Vue项目,值得学习!

安装

接下来我们把Torna运行起来,体验一下它的功能,这里提供Windows和Linux两种安装方式。

Windows

下面我们来介绍Torna在Windows下的安装方法,如果你想深入学习Torna的话可以采用此种方式。

后端运行

  • 首先创建一个数据库torna,然后导入项目中的mysql.sql脚本,导入成功后,表结构如下;

  • 修改项目的配置文件server/boot/src/main/resources/application.properties,修改对应的数据库连接配置;
1
2
3
4
5
6
7
8
9
10
properties复制代码# Server port
server.port=7700

# MySQL host
mysql.host=localhost:3306
# Schema name
mysql.schema=torna
# Insure the account can run CREATE/ALTER sql.
mysql.username=root
mysql.password=root
  • 然后运行项目启动类TornaApplication的main方法,控制台打印如下信息表示启动成功。

前端运行

  • 进入前端项目目录front,运行npm install命令安装依赖;

  • 此时如果遇到node-sass无法安装的情况,可以直接使用如下命令安装;
1
bash复制代码npm i node-sass --sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
  • 依赖安装完成后,可以通过npm run dev命令启动项目,启动成功后访问地址:http://localhost:9530/

  • 通过体验账号密码admin@torna.cn:123456可以访问Torna服务,界面还是不错的!

Linux

在Linux下使用Docker安装Torna是非常简单的,如果你只想用Torna来做API文档服务的话可以采用这种方式。

  • 首先我们需要下载Torna的Docker镜像;
1
bash复制代码docker pull tanghc2020/torna:latest
  • 下载完成后将配置文件application.properties拷贝配置文件到/mydata/torna/config目录下,并修改数据库配置;
1
2
3
4
5
6
7
8
9
10
properties复制代码# Server port
server.port=7700

# MySQL host
mysql.host=192.168.3.101:3306
# Schema name
mysql.schema=torna
# Insure the account can run CREATE/ALTER sql.
mysql.username=root
mysql.password=root
  • 然后通过如下命令运行Torna服务;
1
2
3
bash复制代码docker run -p 7700:7700 --name torna \
-v /mydata/torna/config:/torna/config \
-d tanghc2020/torna:latest
  • 由于镜像中直接包含了前端和后端项目,所以可以直接使用,访问地址:http://192.168.3.101:7700

使用

Torna支持从多种工具导入接口文档,包括Swagger、smart-doc、OpenAPI、Postman等,接下来我们来体验下它的功能!

结合Swagger使用

Torna能大大增强Swagger的功能,并且界面足够美观,下面我们来体验下!

  • 在使用之前,我们需要在Torna中进行配置才行,首先我们来配置一个开放用户,新建一个macro的账号,记住AppKey和Secret;

  • 然后创建一个项目mall-tiny-trona;

  • 接下来创建一个模块,打开OpenAPI标签,获取请求路径和token;

  • 之后在使用Swagger的项目中集成Torna插件,非常简单,添加如下依赖即可;
1
2
3
4
5
6
7
xml复制代码<!-- Torna Swagger 插件 -->
<dependency>
<groupId>cn.torna</groupId>
<artifactId>swagger-plugin</artifactId>
<version>1.2.6</version>
<scope>test</scope>
</dependency>
  • 然后在resources目录下添加配置文件torna.json,配置说明参考注释即可;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
json复制代码{
// 开启推送
"enable": true,
// 扫描package,多个用;隔开
"basePackage": "com.macro.mall.tiny.controller",
// 推送URL,IP端口对应Torna服务器
"url": "http://localhost:7700/api",
// appKey
"appKey": "20211103905498418195988480",
// secret
"secret": "~#ZS~!*2B3I01vbW0f9iKH,rzj-%Xv^Q",
// 模块token
"token": "74365d40038d4f648ae65a077d956836",
// 调试环境,格式:环境名称,调试路径,多个用"|"隔开
"debugEnv": "test,http://localhost:8088",
// 推送人
"author": "macro",
// 打开调试:true/false
"debug": true,
// 是否替换文档,true:替换,false:不替换(追加)。默认:true
"isReplace": true
}
  • 接下来通过调用SwaggerPlugin的pushDoc方法来推送接口文档到Torna;
1
2
3
4
5
6
7
8
9
10
11
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class MallTinyApplicationTests {

@Test
public void pushDoc(){
// 将文档推送到Torna服务中去,默认查找resources下的torna.json
SwaggerPlugin.pushDoc();
}

}
  • 推送成功后,在接口列表将显示如下接口信息;

  • 查看一下接口的详细信息,还是很全面的,界面也不错!

  • 把我们的项目运行起来,就可以直接在上面进行接口调试了,调用下登录接口试试;

  • 如果我们想设置公共请求头的话,比如用于登录认证的Authorization头,可以在模块配置中进行配置;

  • 在后端接口没有完成前,我们如果需要Mock数据的话,可以使用Mock功能;

  • 这里我们对登录接口进行了一下Mock,当然你也可以使用Mock脚本,这下只要接口定义好,前端就可以使用Mock的数据联调了。

结合smart-doc使用

smart-doc是一款无注解侵入的API文档生成工具,具体使用可以参考《smart-doc使用教程》 ,这里介绍下它与Torna结合使用。

  • 首先修改mall-tiny-smart-doc项目的smart-doc配置文件smart-doc.json,添加如下关于Torna的配置;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码{
// torna平台对接appKey
"appKey": "20211103905498418195988480",
//torna平台appToken
"appToken": "b6c50f442eb348f48867d85f4ef2eaea",
//torna平台secret
"secret": "~#ZS~!*2B3I01vbW0f9iKH,rzj-%Xv^Q",
//torna平台地址,填写自己的私有化部署地址
"openUrl": "http://localhost:7700/api",
//测试项目接口环境
"debugEnvName":"测试环境",
//测试项目接口地址
"debugEnvUrl":"http://localhost:8088"
}
  • 由于smart-doc的Maven插件已经自带推送文档到Torna的功能,我们只需双击smart-doc:torna-rest按钮即可;

  • 接下来在Torna中,我们就可以看到相关的接口文档了,非常方便!

总结

当一种工具变得越来越流行,但是某些功能需求又满足不了时,往往会有一些增强工具产生,Torna对于Swagger来说正是这样一种工具。Torna的文档界面和调试功能明显比Swagger高大上多了,而且还增加了权限管理功能,文档的安全性大大增强,大家觉得不错的话可以尝试下它!

参考资料

官方文档:torna.cn/

项目源码地址

github.com/macrozheng/…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【设计模式系列】策略模式看这篇就够了

发表于 2021-11-16

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战

前言

小黑习惯于在讲设计模式时,引入一个具体的业务场景,便于读者能更容易的理解怎么使用。

同样,本期内容先给大家举一个例子,在很多电商网站或者有支付场景的系统中,支持多种支付方式,比如使用银行卡,微信或者支付宝等,那么实际在支付系统内部,不同的支付方式需要请求不同的第三方接口,比如银行卡支付方式需要请求网联,微信支付需要调用微信的API,支付宝则使用支付宝的API。

未使用设计模式

对于上面的场景,如果在没有使用设计模式时,我们的代码一般都是用下面的方式处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class PaymentService {

CreditService creditService;

WeChatService weChatService;

AlipayService alipayService;

public void payment(PaymentType paymentType, BigDecimal amount) {
if (PaymentType.Credit == paymentType) {
creditService.payment();
} else if (PaymentType.WECHAT == paymentType) {
weChatService.payment();
} else if (PaymentType.ALIPAY == paymentType) {
alipayService.payment();
} else {
throw new NotSupportPaymentException("paymentType not support");
}
}
}

enum PaymentType {
Credit, WECHAT, ALIPAY;
}

这种使用if...else的方式虽然能支持现有的业务需求,但是当业务需求发生改变时,比如增加新的支付方式,或者将某一个支付方式下线,则需要对PaymentService进行修改,显然这种设计不符合开闭原则(对修改关闭,对扩展开放),修改之后需要重新对其他的支付方式进行测试。

  • 弊端:
  • 不符合开闭原则
  • 不能做到自由切换
  • if…else逻辑复杂,代码结构混乱
  • 扩展性差
  • 。。。

总之,这种方式就是很low,那么接下来我们使用策略模式来进行改造。

策略模式的定义

策略设计模式是一种行为设计模式。当在处理一个业务时,有多种处理方式,并且需要再运行时决定使哪一种具体实现时,就会使用策略模式。

这个定义和我们的例子说的一回事儿,在支付业务中,有三种付款方式,程序运行时使用哪种方式由用户选择,根据用户选择执行不同的逻辑。

策略模式结构

首先,我们需要将支付方式这一行为抽象为一个策略接口,代表支付方式的抽象。

1
2
3
4
5
java复制代码public interface PaymentStrategy {

public void payment(BigDecimal amount);

}

然后我们再针对需要支持的三种支付方式建立对应的策略实现类。

银行卡支付策略

1
2
3
4
5
6
7
java复制代码public class CreditPaymentStrategy implements PaymentStrategy{
@Override
public void payment(BigDecimal amount) {
System.out.println("使用银行卡支付" + amount);
// 去调用网联接口
}
}

微信支付策略

1
2
3
4
5
6
7
java复制代码public class WechatPaymentStrategy implements PaymentStrategy{
@Override
public void payment(BigDecimal amount) {
System.out.println("使用微信支付" + amount);
// 调用微信支付API
}
}

支付宝支付策略

1
2
3
4
5
6
7
java复制代码public class AlipayPaymentStrategy implements PaymentStrategy {
@Override
public void payment(BigDecimal amount) {
System.out.println("使用支付宝支付" + amount);
// 调用支付宝支付API
}
}

然后重新实现我们的支付服务PaymentService。

1
2
3
4
5
6
7
8
9
java复制代码public class PaymentService {

/**
* 将strategy作为参数传递给支付服务
*/
public void payment(PaymentStrategy strategy, BigDecimal amount) {
strategy.payment(amount);
}
}

发现了吗?我们将支付策略作为参数传递给支付服务,在支付服务中只需要按照运行时传的支付策略对象进行支付就可以了。

我们来测试一下使用策略模式之后的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class StrategyTest {

public static void main(String[] args) {

PaymentService paymentService = new PaymentService();

// 使用微信支付
paymentService.payment(new WechatPaymentStrategy(), new BigDecimal("100"));

//使用支付宝支付
paymentService.payment(new AlipayPaymentStrategy(), new BigDecimal("100"));

}
}

运行结果:

在使用了策略模式之后,在我们的支付服务PaymentService中便不需要写复杂的if...else,如果需要新增加一种支付方式,只需要新增一个新的支付策略实现,这样就满足了开闭原则,并且对其他支付方式的业务逻辑也不会造成影响,扩展性很好。

策略模式类图

按照惯例,我们来看一下策略模式的类图。

JDK中使用策略模式的例子

在JDK中最经典的使用策略模式的例子就是Collections.sort(List<T> list, Comparator<? super T> c)方法,这个方法接受一个比较器Compartor参数,客户端在运行时可以传入一个比较器的实现,sort()方法中根据不同实现,按照不同的方式进行排序。

策略模式使用场景

在实际工作中,会有很多场景可以使用策略模式,比如上面例子中的多个支付方式,再比如与不同的第三方销售渠道对接等等。

总结一下

  • 如果在一个系统里面有许多类,它们仅仅在行为上有区别,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为;
  • 一个系统需要动态地在几种算法中选择一种;
  • 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。

以上就是策略模式的全部内容,如果对你有所帮助,点赞是对我最大的鼓励。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

GraphQL实践2——Spring-GraphQL集成JP

发表于 2021-11-16

介绍

上一篇介绍到用第三方库集成GraphQL,目前spring-graphql项目已经出到1.0.0-M3版本,属于内部预览版,此处尝鲜验证

GraphQL实践1——集成JPA与MySQL - F嘉阳 博客 (fjy8018.top)

集成过程

引入依赖

由于SpringBoot 2.6.0还未发布,因此需要引入较多依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>1.0.0-M3</version>
</dependency>
<!--升级到Spring Boot 2.6后可以移除-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>2.6.0-RC1</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.6.0-RC1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.javax.persistence/hibernate-jpa-2.1-api -->
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.1-api</artifactId>
<version>1.0.2.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.querydsl/querydsl-jpa -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>4.4.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.querydsl/querydsl-apt -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>4.4.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.annotation/javax.annotation-api -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-core</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

引入里程碑仓库

由于还没有GA,所以要引入里程碑仓库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>

引入插件

由于使用了dsl动态生成,还要引入注解处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
xml复制代码<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- added for QueryDsl-->
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

SpringBoot 配置

编写配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
yaml复制代码spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: "****"
url: jdbc:mysql://localhost:3306/sakila?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
jpa:
hibernate:
ddl-auto: none
show-sql: true
database-platform: org.hibernate.dialect.MySQL55Dialect
properties:
hibernate:
format_sql: true
use_sql_comments: true
graphql:
path: /graphql
schema:
locations: classpath:graphql/
fileExtensions: .graphqls, .gqls
printer:
enabled: true
server:
port: 8080

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码package top.fjy8018.graphsqldemo.entity;

import lombok.Data;

import javax.persistence.*;
import java.sql.Timestamp;
import java.util.Objects;

/**
* 演员表实体类
*
* @author F嘉阳
* @date 2021/11/5 10:34
*/
@Data
@Entity
@Table(name = "actor", schema = "sakila")
public class ActorEntity {
@Id
@Column(name = "actor_id", nullable = false)
private Integer actorId;

@Column(name = "first_name", nullable = false, length = 45)
private String firstName;

@Column(name = "last_name", nullable = false, length = 45)
private String lastName;

@Column(name = "last_update", nullable = false)
private Timestamp lastUpdate;

}

DAO

DAO直接继承dsl相关处理器,可自动配对增删改查方法,相比第三方库更加简便

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码package top.fjy8018.graphsqldemo.repository;

import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.graphql.data.GraphQlRepository;
import top.fjy8018.graphsqldemo.entity.ActorEntity;

/**
* 演员表DAO
*
* @author F嘉阳
* @date 2021/11/5 10:35
*/
@GraphQlRepository
public interface ActorEntityRepository extends
CrudRepository<ActorEntity, Integer>, QuerydslPredicateExecutor<ActorEntity> {
}

Graphql资源定义

在resources/graphql下定义接口和实体文件

接口声明和实体定义schema.graphqls

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码type Query {
findOneActor(actorId : ID!): ActorEntity
actorList: [ActorEntity]
}

type ActorEntity {
actorId: ID!
firstName: String!
lastName: String!
lastUpdate: String
}

接口定义acotrEntity.graphql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码query findOneActor($id: ID!) {
findOneActor(actorId: $id) {
actorId
firstName
lastName
lastUpdate
}
}

query actorList {
actorList{
actorId
}
}

启动测试

Spring-Graphql默认不包含可视化界面,此处使用postman进行测试

唯一查询

image-20211115151701244

列表查询

image-20211115151810849

总结

使用Spring官方组件好处在于和Spring生态集成度很高,如果本身就采用Jpa方式进行业务开发,迁移更加方便,需要开发的代码也很少。

样例源码地址

FJiayang/graphql-demo at spring-graphql (github.com)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…328329330…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%