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

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


  • 首页

  • 归档

  • 搜索

再见笨重的ELK!这套轻量级日志收集方案要火!

发表于 2021-07-20

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

摘要

之前一直使用的日志收集方案是ELK,动辄占用几个G的内存,有些配置不好的服务器有点顶不住!最近发现一套轻量级日志收集方案: Loki+Promtail+Grafana(简称LPG), 几百M内存就够了,而且界面也挺不错的,推荐给大家!

简介

LPG日志收集方案内存占用很少,经济且高效!它不像ELK日志系统那样为日志建立索引,而是为每个日志流设置一组标签。下面分别介绍下它的核心组件:

  • Promtail:日志收集器,有点像Filebeat,可以收集日志文件中的日志,并把收集到的数据推送到Loki中去。
  • Loki:聚合并存储日志数据,可以作为Grafana的数据源,为Grafana提供可视化数据。
  • Grafana:从Loki中获取日志信息,进行可视化展示。

日志收集流程图

安装

实现这套日志收集方案需要安装Loki、Promtail、Grafana这些服务,直接使用docker-compose来安装非常方便。

  • 使用的docker-compose.yml脚本如下,直接使用docker-compose命令运行即可;
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
yaml复制代码version: "3"

services:
# 日志存储和解析
loki:
image: grafana/loki
container_name: lpg-loki
volumes:
- /mydata/loki/:/etc/loki/
# 修改loki默认配置文件路径
command: -config.file=/etc/loki/loki.yml
ports:
- 3100:3100

# 日志收集器
promtail:
image: grafana/promtail
container_name: lpg-promtail
volumes:
# 将需要收集的日志所在目录挂载到promtail容器中
- /mydata/app/mall-tiny-loki/logs/:/var/log/
- /mydata/promtail:/etc/promtail/
# 修改promtail默认配置文件路径
command: -config.file=/etc/promtail/promtail.yml

# 日志可视化
grafana:
image: grafana/grafana
container_name: lpg-grafana
ports:
- 3000:3000
  • 由于我们把Loki和Promtail的配置文件挂载到了宿主机上,在运行之前,需要先准备好这两个配置文件;
  • Loki的配置文件/mydata/loki/loki.yml内容如下,使用的是默认配置(可以先不挂载配置文件运行Loki的Docker容器,然后从容器中拷贝出来即可);
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
yaml复制代码auth_enabled: false

server:
http_listen_port: 3100

ingester:
lifecycler:
address: 127.0.0.1
ring:
kvstore:
store: inmemory
replication_factor: 1
final_sleep: 0s
chunk_idle_period: 1h # Any chunk not receiving new logs in this time will be flushed
max_chunk_age: 1h # All chunks will be flushed when they hit this age, default is 1h
chunk_target_size: 1048576 # Loki will attempt to build chunks up to 1.5MB, flushing first if chunk_idle_period or max_chunk_age is reached first
chunk_retain_period: 30s # Must be greater than index read cache TTL if using an index cache (Default index read cache TTL is 5m)
max_transfer_retries: 0 # Chunk transfers disabled

schema_config:
configs:
- from: 2020-10-24
store: boltdb-shipper
object_store: filesystem
schema: v11
index:
prefix: index_
period: 24h

storage_config:
boltdb_shipper:
active_index_directory: /loki/boltdb-shipper-active
cache_location: /loki/boltdb-shipper-cache
cache_ttl: 24h # Can be increased for faster performance over longer query periods, uses more disk space
shared_store: filesystem
filesystem:
directory: /loki/chunks

compactor:
working_directory: /loki/boltdb-shipper-compactor
shared_store: filesystem

limits_config:
reject_old_samples: true
reject_old_samples_max_age: 168h

chunk_store_config:
max_look_back_period: 0s

table_manager:
retention_deletes_enabled: false
retention_period: 0s

ruler:
storage:
type: local
local:
directory: /loki/rules
rule_path: /loki/rules-temp
alertmanager_url: http://localhost:9093
ring:
kvstore:
store: inmemory
enable_api: true
  • Promtail的配置文件/mydata/loki/promtail.yml内容如下,使用的也是默认配置,这里的clients.url需要注意下,由于我们使用的是docker-compose部署,所以可以将服务名称loki作为域名来访问Loki服务;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yaml复制代码server:
http_listen_port: 9080
grpc_listen_port: 0

positions:
filename: /tmp/positions.yaml

clients:
- url: http://loki:3100/loki/api/v1/push

scrape_configs:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: varlogs
__path__: /var/log/*log
  • 运行docker-compose.yml脚本安装所有服务,使用如下命令即可;
1
bash复制代码docker-compose up -d
  • 运行成功后,可以使用docker ps |grep lpg命令查看服务状态。
1
2
3
4
bash复制代码[root@local-linux lpg]# docker ps |grep lpg
64761b407423 grafana/loki "/usr/bin/loki -conf…" 3 minutes ago Up 3 minutes 0.0.0.0:3100->3100/tcp lpg-loki
67f0f0912971 grafana/grafana "/run.sh" 3 minutes ago Up 3 minutes 0.0.0.0:3000->3000/tcp lpg-grafana
f2d78eb188d1 grafana/promtail "/usr/bin/promtail -…" 3 minutes ago Up 3 minutes lpg-promtail

使用

接下来我们将使用LPG日志收集系统来收集SpringBoot应用的日志,SpringBoot应用基本不用做特殊配置。

  • 首先创建一个SpringBoot应用,修改配置文件application.yml,将日志输出到/var/logs目录下;
1
2
3
4
5
6
7
8
yaml复制代码spring:
application:
name: mall-tiny-loki

logging:
path: /var/logs
level:
com.macro.mall.tiny: debug
  • 使用如下命令运行SpringBoot应用,并把日志目录挂载到宿主机上,这样Promtail就可以收集到日志了;
1
2
3
4
5
bash复制代码docker run -p 8088:8088 --name mall-tiny-loki \
-v /etc/localtime:/etc/localtime \
-v /mydata/app/mall-tiny-loki/logs:/var/logs \
-e TZ="Asia/Shanghai" \
-d mall-tiny/mall-tiny-loki:1.0-SNAPSHOT
  • 运行成功后登录Grafana,账号密码为admin:admin,登录成功后需要添加Loki为数据源,访问地址:http://192.168.7.149:3000/

  • 在数据源选择界面中直接选择Loki,我们可以看到Grafana也支持使用Elasticsearch作为数据源;

  • 之后设置下你的Loki访问地址,点击Save&test保存并测试,显示绿色提示信息表示设置成功,Loki访问地址:http://192.168.7.149:3100

  • 接下来在Explore选择Loki,并输入查询表达式(Loki query)为{filename="/var/log/spring.log"},就可以查看我们的SpringBoot应用输出的日志了。

总结

本文主要介绍了LPG日志系统的搭建及使用它收集SpringBoot应用的日志,LPG日志收集方案确实非常轻量级,性能也不错!不过如果你有对日志进行全文搜索的需求的话,还是得使用ELK系统。如果你对Grafana还不熟悉的话,可以参考下这篇文章《号称下一代可视化监控系统,结合SpringBoot使用,贼爽!》。

参考资料

  • Loki官方文档:grafana.com/docs/loki/l…
  • Promtail官方文档:grafana.com/docs/loki/l…

项目源码地址

github.com/macrozheng/…

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

本文转载自: 掘金

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

解决mysql死锁错误 SQLSTATE【HY000】 G

发表于 2021-07-20

​

SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction

在PHP调试时 提交事务触发异常后没有执行回滚导致mysql死锁,以致后续请求更新不了数据

问题原因

在mysql中事务a执行修改数据,比如: update table set a=1 where id=1;此时事务并未进行提交也没有回滚,然后事务B开始运行,修改同一条数据: update table set a=2 where id=1;

此时b一直等待a释放锁直到超时,超过设置的超时时间后会产生报错

问题出现环境:

1、在同一事务内先后对同一条数据进行插入和更新操作;

2、多台服务器操作同一数据库;

3、瞬时出现高并发现象;

4、语句中没有执行commit,也没有rollback就return退出了

比如参数检查不通过,直接return错误信息,导致回滚不能执行

如以下代码先执行了更新操作,后面出错又直接返回,导致没有执行rollback,对于这种操作return前一点要回滚,或者抛出异常统一扑捉后返回错误信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
php复制代码$this->startTrans();
try {


$user = new User();
$user->where('id',$userId)->update(['realname'=>$parentName]);

$existId = $this->where('class_id',$classId)->where('student_number',$number)->find();
if ($existId)
return ['data' => '', 'code' => 300, 'msg' => '学号重复'];
$this->commit();

} catch (Throwable $e) {
$this->rollback();
return ['data' => '', 'code' => 20102, 'msg' => $e->getMessage()];
}

异常信息

SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction

)​

解决

1、通过下面语句查找到为提交事务的数据,kill此线程即可。

1
csharp复制代码select * from information_schema.innodb_trx

)​

trx_mysql_thread_id 即为该进程

1
bash复制代码kill 1544

相关排查语句

show full processlist ## 当前连接的线程

select * from information_schema.innodb_trx ## 当前运行的所有事务

select * from information_schema.innodb_locks ## 当前出现的锁

select * from information_schema.innodb_lock_waits ## 锁等待的对应关系

字段详细信息

desc information_schema.innodb_locks;

Field Type Null Key Default Remark

lock_id varchar(81) NO 锁ID

lock_trx_id varchar(18) NO 拥有锁的事务ID

lock_mode varchar(32) NO 锁模式

lock_type varchar(32) NO 锁类型

lock_table varchar(1024) NO 被锁的表

lock_index varchar(1024) YES NULL 被锁的索引

lock_space bigint(21) unsigned YES NULL 被锁的表空间号

lock_page bigint(21) unsigned YES NULL 被锁的页号

lock_rec bigint(21) unsigned YES NULL 被锁的记录号

lock_data varchar(8192) YES NULL 被锁的数据

desc information_schema.innodb_lock_waits

Field Type Null Key Default Remark

requesting_trx_id varchar(18) NO 请求锁的事务ID

requested_lock_id varchar(81) NO 请求锁的锁ID

blocking_trx_id varchar(18) NO 当前拥有锁的事务ID

blocking_lock_id varchar(81) NO 当前拥有锁的锁ID

desc information_schema.innodb_trx ;

Field Type Null Key Default Extra Remark

trx_id varchar(18) NO 事务ID

trx_state varchar(13) NO 事务状态:

trx_started datetime NO 0000-00-00 00:00:00 事务开始时间;

trx_requested_lock_id varchar(81) YES NULL innodb_locks.lock_id

trx_wait_started datetime YES NULL 事务开始等待的时间

trx_weight bigint(21) unsigned NO 0 #

trx_mysql_thread_id bigint(21) unsigned NO 0 事务线程ID

trx_query varchar(1024) YES NULL 具体SQL语句

trx_operation_state varchar(64) YES NULL 事务当前操作状态

trx_tables_in_use bigint(21) unsigned NO 0 事务中有多少个表被使用

trx_tables_locked bigint(21) unsigned NO 0 事务拥有多少个锁

trx_lock_structs bigint(21) unsigned NO 0 #

trx_lock_memory_bytes bigint(21) unsigned NO 0 事务锁住的内存大小(B)

trx_rows_locked bigint(21) unsigned NO 0 事务锁住的行数

trx_rows_modified bigint(21) unsigned NO 0 事务更改的行数

trx_concurrency_tickets bigint(21) unsigned NO 0 事务并发票数

trx_isolation_level varchar(16) NO 事务隔离级别

trx_unique_checks int(1) NO 0 是否唯一性检查

trx_foreign_key_checks int(1) NO 0 是否外键检查

trx_last_foreign_key_error varchar(256) YES NULL 最后的外键错误

trx_adaptive_hash_latched int(1) NO 0 #

trx_adaptive_hash_timeout bigint(21) unsigned NO 0 #3

​

本文转载自: 掘金

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

归约、分组与分区,深入讲解JavaStream终结操作

发表于 2021-07-20

本文为掘金社区首发签约文章,未获授权禁止转载。

思维导图镇楼,先感谢大家对我上一篇文的积极点赞,助我完成KPI😄。

上一篇中给大家讲了Stream的前半部分知识——包括对Stream的整体概览及Stream的创建和Stream的转换流操作,并对Stream一些内部优化点做了简明的说明。

虽迟但到,今天就来继续给大家更Stream第二部分知识——终结操作,由于这部分的API内容繁多且复杂,所以我单开一篇给大家细细讲讲,我的文章很长,请大家忍耐一下。

正式开始之前,我们先来说说聚合方法本身的特性(接下来我将用聚合方法代指终结操作中的方法):

  1. 聚合方法代表着整个流计算的最终结果,所以它的返回值都不是Stream。
  2. 聚合方法返回值可能为空,比如filter没有匹配到的情况,JDK8中用Optional来规避NPE。
  3. 聚合方法都会调用evaluate方法,这是一个内部方法,看源码的过程中可以用它来判定一个方法是不是聚合方法。

ok,知晓了聚合方法的特性,我为了便于理解,又将聚合方法分为几大类:

其中简单聚合方法我会简单讲解,其它则会着重讲解,尤其是收集器,它能做的实在太多了。。。

Stream的聚合方法是我们在使用Stream中的必用操作,认真学习本篇,不说马上就能对Stream得心应手,起码也可以行云流水吧😄

  1. 简单聚合方法

第一节嘛,先来点简单的。

Stream的聚合方法比上一篇讲过的无状态和有状态方法都要多,但是其中也有一些是喵一眼就能学会的,第一节我们先来说说这部分方法:

  • count():返回Stream中元素的size大小。
  • forEach():通过内部循环Stream中的所有元素,对每一个元素进行消费,此方法没有返回值。
  • forEachOrder():和上面方法的效果一样,但是这个可以保持消费顺序,哪怕是在多线程环境下。
  • anyMatch(Predicate predicate):这是一个短路操作,通过传入断言参数判断是否有元素能够匹配上断言。
  • allMatch(Predicate predicate):这是一个短路操作,通过传入断言参数返回是否所有元素都能匹配上断言。
  • noneMatch(Predicate predicate):这是一个短路操作,通过传入断言参数判断是否所有元素都无法匹配上断言,如果是则返回true,反之则false。
  • findFirst():这是一个短路操作,返回Stream中的第一个元素,Stream可能为空所以返回值用Optional处理。
  • findAny():这是一个短路操作,返回Stream中的任意一个元素,串型流中一般是第一个元素,Stream可能为空所以返回值用Optional处理。

虽然以上都比较简单,但是这里面有五个涉及到短路操作的方法我还是想提两嘴:

首先是findFirst()和findAny()这两个方法, 由于它们只需要拿到一个元素就能方法就能结束,所以短路效果很好理解。

接着是anyMatch方法,它只需要匹配到一个元素方法也能结束,所以它的短路效果也很好理解。

最后是allMatch方法和noneMatch,乍一看这两个方法都是需要遍历整个流中的所有元素的,其实不然,比如allMatch只要有一个元素不匹配断言它就可以返回false了,noneMatch只要有一个元素匹配上断言它也可以返回false了,所以它们都是具有短路效果的方法。

  1. 归约

2.1 reduce:反复求值

第二节我们来说说归约,由于这个词过于抽象,我不得不找了一句通俗易懂的解释来翻译这句话,下面是归约的定义:

将一个Stream中的所有元素反复结合起来,得到一个结果,这样的操作被称为归约。

注:在函数式编程中,这叫做折叠( fold )。

举个很简单的例子,我有1、2、3三个元素,我把它们俩俩相加,最后得出6这个数字,这个过程就是归约。

再比如,我有1、2、3三个元素,我把它们俩俩比较,最后挑出最大的数字3或者挑出最小的数字1,这个过程也是归约。

下面我举一个求和的例子来演示归约,归约使用reduce方法:

1
2
java复制代码        Optional<Integer> reduce = List.of(1, 2, 3).stream()
.reduce((i1, i2) -> i1 + i2);

首先你可能注意到了,我在上文的小例子中一直在用俩俩这个词,这代表归约是俩俩的元素进行处理然后得到一个最终值,所以reduce的方法的参数是一个二元表达式,它将两个参数进行任意处理,最后得到一个结果,其中它的参数和结果必须是同一类型。

比如代码中的,i1和i2就是二元表达式的两个参数,它们分别代表元素中的第一个元素和第二个元素,当第一次相加完成后,所得的结果会赋值到i1身上,i2则会继续代表下一个元素,直至元素耗尽,得到最终结果。

如果你觉得这么写不够优雅,也可以使用Integer中的默认方法:

1
2
java复制代码        Optional<Integer> reduce = List.of(1, 2, 3).stream()
.reduce(Integer::sum);

这也是一个以方法引用代表lambda表达式的例子。

你可能还注意到了,它们的返回值是Optional的,这是预防Stream没有元素的情况。

你也可以想办法去掉这种情况,那就是让元素中至少要有一个值,这里reduce提供一个重载方法给我们:

1
2
java复制代码        Integer reduce = List.of(1, 2, 3).stream()
.reduce(0, (i1, i2) -> i1 + i2);

如上例,在二元表达式前面多加了一个参数,这个参数被称为初始值,这样哪怕你的Stream没有元素它最终也会返回一个0,这样就不需要Optional了。

在实际方法运行中,初始值会在第一次执行中占据i1的位置,i2则代表Stream中的第一个元素,然后所得的和再次占据i1的位置,i2代表下一个元素。

不过使用初始值不是没有成本的,它应该符合一个原则:accumulator.apply(identity, i1) == i1,也就是说在第一次执行的时候,它的返回结果都应该是你Stream中的第一个元素。

比如我上面的例子是一个相加操作,则第一次相加时就是0 + 1 = 1,符合上面的原则,作此原则是为了保证并行流情况下能够得到正确的结果。

如果你的初始值是1,则在并发情况下每个线程的初始化都是1,那么你的最终和就会比你预想的结果要大。

2.2 max:利用归约求最大

max方法也是一个归约方法,它是直接调用了reduce方法。

先来看一个示例:

1
2
3
4
5
6
7
8
java复制代码        Optional<Integer> max = List.of(1, 2, 3).stream()
.max((a, b) -> {
if (a > b) {
return 1;
} else {
return -1;
}
});

没错,这就是max方法用法,这让我觉得我不是在使用函数式接口,当然你也可以使用Integer的方法进行简化:

1
2
java复制代码        Optional<Integer> max = List.of(1, 2, 3).stream()
.max(Integer::compare);

哪怕如此,这个方法依旧让我感觉到很繁琐,我虽然可以理解在max方法里面传参数是为了让我们自己自定义排序规则,但我不理解为什么没有一个默认按照自然排序进行排序的方法,而是非要让我传参数。

直到后来我想到了基础类型Stream,果然,它们里面是可以无需传参直接拿到最大值:

1
java复制代码        OptionalLong max = LongStream.of(1, 2, 3).max();

果然,我能想到的,类库设计者都想到了~

注 :OptionalLong是Optional对基础类型long的封装。

2.3 min:利用归约求最小

min还是直接看例子吧:

1
2
java复制代码        Optional<Integer> max = List.of(1, 2, 3).stream()
.min(Integer::compare);

它和max区别就是底层把 > 换成了 <,过于简单,不再赘述。

  1. 收集器

第三节我们来看看收集器,它的作用是对Stream中的元素进行收集而形成一个新的集合。

虽然我在本篇开头的时候已经给过一张思维导图了,但是由于收集器的API比较多所以我又画了一张,算是对开头那张的补充:

收集器的方法名是collect,它的方法定义如下:

1
java复制代码    <R, A> R collect(Collector<? super T, A, R> collector);

顾名思义,收集器是用来收集Stream的元素的,最后收集成什么我们可以自定义,但是我们一般不需要自己写,因为JDK内置了一个Collector的实现类——Collectors。

3.1 收集方法

通过Collectors我们可以利用它的内置方法很方便的进行数据收集:

比如你想把元素收集成集合,那么你可以使用toCollection或者toList方法,不过我们一般不使用toCollection,因为它需要传参数,没人喜欢传参数。

你也可以使用toUnmodifiableList,它和toList区别就是它返回的集合不可以改变元素,比如删除或者新增。

再比如你要把元素去重之后收集起来,那么你可以使用toSet或者toUnmodifiableSet。

接下来放一个比较简单的例子:

1
2
3
4
5
6
7
8
9
10
11
java复制代码        // toList
List.of(1, 2, 3).stream().collect(Collectors.toList());

// toUnmodifiableList
List.of(1, 2, 3).stream().collect(Collectors.toUnmodifiableList());

// toSet
List.of(1, 2, 3).stream().collect(Collectors.toSet());

// toUnmodifiableSet
List.of(1, 2, 3).stream().collect(Collectors.toUnmodifiableSet());

以上这些方法都没有参数,拿来即用,toList底层也是经典的ArrayList,toSet 底层则是经典的HashSet。


也许有时候你也许想要一个收集成一个Map,比如通过将订单数据转成一个订单号对应一个订单,那么你可以使用toMap():

1
2
3
4
java复制代码        List<Order> orders = List.of(new Order(), new Order());

Map<String, Order> map = orders.stream()
.collect(Collectors.toMap(Order::getOrderNo, order -> order));

toMap() 具有两个参数:

  1. 第一个参数代表key,它表示你要设置一个Map的key,我这里指定的是元素中的orderNo。
  2. 第二个参数代表value,它表示你要设置一个Map的value,我这里直接把元素本身当作值,所以结果是一个Map<String, Order>。

你也可以将元素的属性当作值:

1
2
3
4
java复制代码        List<Order> orders = List.of(new Order(), new Order());

Map<String, List<Item>> map = orders.stream()
.collect(Collectors.toMap(Order::getOrderNo, Order::getItemList));

这样返回的就是一个订单号+商品列表的Map了。

toMap() 还有两个伴生方法:

  • toUnmodifiableMap():返回一个不可修改的Map。
  • toConcurrentMap():返回一个线程安全的Map。

这两个方法和toMap() 的参数一模一样,唯一不同的就是底层生成的Map特性不太一样,我们一般使用简简单单的toMap() 就够了,它的底层是我们最常用的HashMap() 实现。

toMap() 功能虽然强大也很常用,但是它却有一个致命缺点。

我们知道HahsMap遇到相同的key会进行覆盖操作,但是toMap() 方法生成Map时如果你指定的key出现了重复,那么它会直接抛出异常。

比如上面的订单例子中,我们假设两个订单的订单号一样,但是你又将订单号指定了为key,那么该方法会直接抛出一个IllegalStateException,因为它不允许元素中的key是相同的。

3.2 分组方法

如果你想对数据进行分类,但是你指定的key是可以重复的,那么你应该使用groupingBy 而不是toMap。

举个简单的例子,我想对一个订单集合以订单类型进行分组,那么可以这样:

1
2
3
4
java复制代码        List<Order> orders = List.of(new Order(), new Order());

Map<Integer, List<Order>> collect = orders.stream()
.collect(Collectors.groupingBy(Order::getOrderType));

直接指定用于分组的元素属性,它就会自动按照此属性进行分组,并将分组的结果收集为一个List。

1
2
3
4
java复制代码        List<Order> orders = List.of(new Order(), new Order());

Map<Integer, Set<Order>> collect = orders.stream()
.collect(Collectors.groupingBy(Order::getOrderType, toSet()));

groupingBy还提供了一个重载,让你可以自定义收集器类型,所以它的第二个参数是一个Collector收集器对象。

对于Collector类型,我们一般还是使用Collectors类,这里由于我们前面已经使用了Collectors,所以这里不必声明直接传入一个toSet()方法,代表我们将分组后的元素收集为Set。

groupingBy还有一个相似的方法叫做groupingByConcurrent(),这个方法可以在并行时提高分组效率,但是它是不保证顺序的,这里就不展开讲了。

3.3 分区方法

接下来我将介绍分组的另一种情况——分区,名字有点绕,但意思很简单:

将数据按照TRUE或者FALSE进行分组就叫做分区。

举个例子,我们将一个订单集合按照是否支付进行分组,这就是分区:

1
2
3
4
java复制代码        List<Order> orders = List.of(new Order(), new Order());

Map<Boolean, List<Order>> collect = orders.stream()
.collect(Collectors.partitioningBy(Order::getIsPaid));

因为订单是否支付只具有两种状态:已支付和未支付,这种分组方式我们就叫做分区。

和groupingBy一样,它还具有一个重载方法,用来自定义收集器类型:

1
2
3
4
java复制代码        List<Order> orders = List.of(new Order(), new Order());

Map<Boolean, Set<Order>> collect = orders.stream()
.collect(Collectors.partitioningBy(Order::getIsPaid, toSet()));

3.4 经典复刻方法

终于来到最后一节了,请原谅我给这部分的方法起了一个这么土的名字,但是这些方法确实如我所说:经典复刻。

换言之,就是Collectors把Stream原先的方法又实现了一遍,包括:

  1. map → mapping
  2. filter → filtering
  3. flatMap → flatMapping
  4. count → counting
  5. reduce → reducing
  6. max → maxBy
  7. **min ** → minBy

这些方法的功能我就不一一列举了,之前的文章已经讲的很详尽了,唯一的不同是某些方法多了一个参数,这个参数就是我们在分组和分区里面讲过的收集参数,你可以指定收集到什么容器内。

我把它们抽出来主要想说的为什么要复刻这么多方法处理,这里我说说个人见解,不代表官方意见。

我觉得主要是为了功能的组合。

什么意思呢?比方说我又有一个需求:使用订单类型对订单进行分组,并找出每组有多少个订单。

订单分组我们已经讲过了,找到其每组有多少订单只要拿到对应list的size就行了,但是我们可以不这么麻烦,而是一步到位,在输出结果的时候键值对就是订单类型和订单数量:

1
2
java复制代码        Map<Integer, Long> collect = orders.stream()
.collect(Collectors.groupingBy(Order::getOrderType, counting()));

就这样,就这么简单,就好了,这里等于说我们对分组后的数据又进行了一次计数操作。

上面的这个例子可能不对明显,当我们需要对最后收集之后的数据在进行操作时,一般我们需要重新将其转换成Stream然后操作,但是使用Collectors的这些方法就可以让你很方便的在Collectors中进行数据的处理。

再举个例子,还是通过订单类型对订单进行分组,但是呢,我们想要拿到每种类型订单金额最大的那个,那么我们就可以这样:

1
2
3
4
5
java复制代码        List<Order> orders = List.of(new Order(), new Order());        

Map<Integer, Optional<Order>> collect2 = orders.stream()
.collect(groupingBy(Order::getOrderType,
maxBy(Comparator.comparing(Order::getMoney))));

更简洁,也更方便,不需要我们分组完之后再去一一寻找最大值了,可以一步到位。

再来一个分组之后,求各组订单金额之后的:

1
2
3
4
java复制代码        List<Order> orders = List.of(new Order(), new Order());        

Map<Integer, Long> collect = orders.stream()
.collect(groupingBy(Order::getOrderType, summingLong(Order::getMoney)));

不过summingLong这里我们没有讲,它就是一个内置的请和操作,支持Integer、Long和Double。

还有一个类似的方法叫做averagingLong看名字就知道,求平均的,都比较简单,建议大家没事的时候可以扫两眼。


该结束了,最后一个方法joining(),用来拼接字符串很实用:

1
2
3
4
java复制代码        List<Order> orders = List.of(new Order(), new Order());

String collect = orders.stream()
.map(Order::getOrderNo).collect(Collectors.joining(","));

这个方法的方法名看着有点眼熟,没错,String类在JDK8之后新加了一个join() 方法,也是用来拼接字符串的,Collectors的joining不过和它功能一样,底层实现也一样,都用了StringJoiner类。

  1. 总结

终于写完了。

在这篇Stream中终结操作中,我提了Stream中的所有聚合方法,可以说你看完了这篇,Stream的所有聚合操作就掌握个七七八八了,不会用没关系,就知道有这个东西了就行了,不然在你的知识体系中Stream根本做不了XX事,就有点贻笑大方了。

当然,我还是建议大家在项目中多多用用这些简练的API,提升代码可读性,也更加简练,被review的时候也容易让别人眼前一亮~

看到这的掘友,希望高抬贵手帮我点个赞,为我的掘金KPI大业出一份力,你们的支持就是我创作的不竭动力,我们下期见。


参考书籍:

  • Java8实战

推荐文章:

  • 延迟执行与不可变,系统讲解JavaStream数据处理

本文转载自: 掘金

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

SpringBoot 如何统一后端返回格式?老鸟们都是这样玩

发表于 2021-07-20

大家好,我是飘渺。

今天我们来聊一聊在基于SpringBoot前后端分离开发模式下,如何友好的返回统一的标准格式以及如何优雅的处理全局异常。

首先我们来看看为什么要返回统一的标准格式?

为什么要对SpringBoot返回统一的标准格式

在默认情况下,SpringBoot的返回格式常见的有三种:

第一种:返回 String

1
2
3
4
java复制代码@GetMapping("/hello")
public String getStr(){
return "hello,javadaily";
}

此时调用接口获取到的返回值是这样:

1
java复制代码hello,javadaily

第二种:返回自定义对象

1
2
3
4
5
java复制代码@GetMapping("/aniaml")
public Aniaml getAniaml(){
Aniaml aniaml = new Aniaml(1,"pig");
return aniaml;
}

此时调用接口获取到的返回值是这样:

1
2
3
4
java复制代码{
"id": 1,
"name": "pig"
}

第三种:接口异常

1
2
3
4
5
csharp复制代码@GetMapping("/error")
public int error(){
int i = 9/0;
return i;
}

此时调用接口获取到的返回值是这样:

1
2
3
4
5
6
json复制代码{
"timestamp": "2021-07-08T08:05:15.423+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/wrong"
}

基于以上种种情况,如果你和前端开发人员联调接口她们就会很懵逼,由于我们没有给他一个统一的格式,前端人员不知道如何处理返回值。

还有甚者,有的同学比如小张喜欢对结果进行封装,他使用了Result对象,小王也喜欢对结果进行包装,但是他却使用的是Response对象,当出现这种情况时我相信前端人员一定会抓狂的。

所以我们项目中是需要定义一个统一的标准返回格式的。

定义返回标准格式

一个标准的返回格式至少包含3部分:

  1. status 状态值:由后端统一定义各种返回结果的状态码
  2. message 描述:本次接口调用的结果描述
  3. data 数据:本次返回的数据。
1
2
3
4
5
json复制代码{
"status":"100",
"message":"操作成功",
"data":"hello,javadaily"
}

当然也可以按需加入其他扩展值,比如我们就在返回对象中添加了接口调用时间

  1. timestamp: 接口调用时间

定义返回对象

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
java复制代码@Data
public class ResultData<T> {
/** 结果状态 ,具体状态码参见ResultData.java*/
private int status;
private String message;
private T data;
private long timestamp ;


public ResultData (){
this.timestamp = System.currentTimeMillis();
}


public static <T> ResultData<T> success(T data) {
ResultData<T> resultData = new ResultData<>();
resultData.setStatus(ReturnCode.RC100.getCode());
resultData.setMessage(ReturnCode.RC100.getMessage());
resultData.setData(data);
return resultData;
}

public static <T> ResultData<T> fail(int code, String message) {
ResultData<T> resultData = new ResultData<>();
resultData.setStatus(code);
resultData.setMessage(message);
return resultData;
}

}

定义状态码

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
scss复制代码public enum ReturnCode {
/**操作成功**/
RC100(100,"操作成功"),
/**操作失败**/
RC999(999,"操作失败"),
/**服务限流**/
RC200(200,"服务开启限流保护,请稍后再试!"),
/**服务降级**/
RC201(201,"服务开启降级保护,请稍后再试!"),
/**热点参数限流**/
RC202(202,"热点参数限流,请稍后再试!"),
/**系统规则不满足**/
RC203(203,"系统规则不满足要求,请稍后再试!"),
/**授权规则不通过**/
RC204(204,"授权规则不通过,请稍后再试!"),
/**access_denied**/
RC403(403,"无访问权限,请联系管理员授予权限"),
/**access_denied**/
RC401(401,"匿名用户访问无权限资源时的异常"),
/**服务异常**/
RC500(500,"系统异常,请稍后重试"),

INVALID_TOKEN(2001,"访问令牌不合法"),
ACCESS_DENIED(2003,"没有权限访问该资源"),
CLIENT_AUTHENTICATION_FAILED(1001,"客户端认证失败"),
USERNAME_OR_PASSWORD_ERROR(1002,"用户名或密码错误"),
UNSUPPORTED_GRANT_TYPE(1003, "不支持的认证模式");



/**自定义状态码**/
private final int code;
/**自定义描述**/
private final String message;

ReturnCode(int code, String message){
this.code = code;
this.message = message;
}


public int getCode() {
return code;
}

public String getMessage() {
return message;
}
}

统一返回格式

1
2
3
4
java复制代码@GetMapping("/hello")
public ResultData<String> getStr(){
return ResultData.success("hello,javadaily");
}

此时调用接口获取到的返回值是这样:

1
2
3
4
5
6
7
json复制代码{
"status": 100,
"message": "hello,javadaily",
"data": null,
"timestamp": 1625736481648,
"httpStatus": 0
}

这样确实已经实现了我们想要的结果,我在很多项目中看到的都是这种写法,在Controller层通过 ResultData.success()对返回结果进行包装后返回给前端。

看到这里我们不妨停下来想想,这样做有什么弊端呢?

最大的弊端就是我们后面每写一个接口都需要调用 ResultData.success()这行代码对结果进行包装,重复劳动,浪费体力;而且还很容易被其他老鸟给嘲笑。

image-20210716084136689

所以呢我们需要对代码进行优化,目标就是不要每个接口都手工制定 ResultData返回值。

高级实现方式

要优化这段代码很简单,我们只需要借助SpringBoot提供的 ResponseBodyAdvice即可。

ResponseBodyAdvice的作用:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。

先来看下 ResponseBodyAdvice的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public interface ResponseBodyAdvice<T> {
/**
* 是否支持advice功能
* true 支持,false 不支持
*/
boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2);

/**
* 对返回的数据进行处理
*/
@Nullable
T beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4, ServerHttpRequest var5, ServerHttpResponse var6);
}

我们只需要编写一个具体实现类即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码/**
* @author jam
* @date 2021/7/8 10:10 上午
*/
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;

@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}

@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if(o instanceof String){
return objectMapper.writeValueAsString(ResultData.success(o));
}
return ResultData.success(o);
}
}

需要注意两个地方:

  • @RestControllerAdvice注解

@RestControllerAdvice是 @RestController注解的增强,可以实现三个方面的功能:

1. 全局异常处理
2. 全局数据绑定
3. 全局数据预处理
  • String类型判断
1
2
3
java复制代码if(o instanceof String){
return objectMapper.writeValueAsString(ResultData.success(o));
}

这段代码一定要加,如果Controller直接返回String的话,SpringBoot是直接返回,故我们需要手动转换成json。

经过上面的处理我们就再也不需要通过 ResultData.success()来进行转换了,直接返回原始数据格式,SpringBoot自动帮我们实现包装类的封装。

1
2
3
4
java复制代码@GetMapping("/hello")
public String getStr(){
return "hello,javadaily";
}

此时我们调用接口返回的数据结果为:

1
2
3
4
5
6
json复制代码{
"status": 100,
"message": "操作成功",
"data": "hello,javadaily",
"timestamp": 1626427373113
}

是不是感觉很完美,别急,还有个问题在等着你呢。

image-20210716084552589

接口异常问题

此时有个问题,由于我们没对Controller的异常进行处理,当我们调用的方法一旦出现异常,就会出现问题,比如下面这个接口

1
2
3
4
5
java复制代码@GetMapping("/wrong")
public int error(){
int i = 9/0;
return i;
}

返回的结果为:

image-20210708191106503

这显然不是我们想要的结果,接口都报错了还返回操作成功的响应码,前端看了会打人的。

别急,接下来我们进入第二个议题,如何优雅的处理全局异常。

SpringBoot为什么需要全局异常处理器

  1. 不用手写try…catch,由全局异常处理器统一捕获

使用全局异常处理器最大的便利就是程序员在写代码时不再需要手写 try...catch了,前面我们讲过,默认情况下SpringBoot出现异常时返回的结果是这样:

1
2
3
4
5
6
json复制代码{
"timestamp": "2021-07-08T08:05:15.423+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/wrong"
}
1
复制代码这种数据格式返回给前端,前端是看不懂的,所以这时候我们一般通过

try...catch来处理异常

1
2
3
4
5
6
7
8
9
10
11
java复制代码@GetMapping("/wrong")
public int error(){
int i;
try{
i = 9/0;
}catch (Exception e){
log.error("error:{}",e);
i = 0;
}
return i;
}

我们追求的目标肯定是不需要再手动写 try...catch了,而是希望由全局异常处理器处理。

  1. 对于自定义异常,只能通过全局异常处理器来处理
1
2
3
4
java复制代码@GetMapping("error1")
public void empty(){
throw new RuntimeException("自定义异常");
}
  1. 当我们引入Validator参数校验器的时候,参数校验不通过会抛出异常,此时是无法用 try...catch捕获的,只能使用全局异常处理器。

SpringBoot集成参数校验请参考这篇文章SpringBoot开发秘籍 - 集成参数校验及高阶技巧

如何实现全局异常处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
/**
* 默认全局异常处理。
* @param e the e
* @return ResultData
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultData<String> exception(Exception e) {
log.error("全局异常信息 ex={}", e.getMessage(), e);
return ResultData.fail(ReturnCode.RC500.getCode(),e.getMessage());
}

}

有三个细节需要说明一下:

  1. @RestControllerAdvice,RestController的增强类,可用于实现全局异常处理器
  2. @ExceptionHandler,统一处理某一类异常,从而减少代码重复率和复杂度,比如要获取自定义异常可以@ExceptionHandler(BusinessException.class)
  3. @ResponseStatus指定客户端收到的http状态码

体验效果

这时候我们调用如下接口:

1
2
3
4
java复制代码@GetMapping("error1")
public void empty(){
throw new RuntimeException("自定义异常");
}

返回的结果如下:

1
2
3
4
5
6
json复制代码{
"status": 500,
"message": "自定义异常",
"data": null,
"timestamp": 1625795902556
}

基本满足我们的需求了。

但是当我们同时启用统一标准格式封装功能 ResponseAdvice和 RestExceptionHandler全局异常处理器时又出现了新的问题:

1
2
3
4
5
6
7
8
9
10
11
json复制代码{
"status": 100,
"message": "操作成功",
"data": {
"status": 500,
"message": "自定义异常",
"data": null,
"timestamp": 1625796167986
},
"timestamp": 1625796168008
}

此时返回的结果是这样,统一格式增强功能会给返回的异常结果再次封装,所以接下来我们需要解决这个问题。

全局异常接入返回的标准格式

要让全局异常接入标准格式很简单,因为全局异常处理器已经帮我们封装好了标准格式,我们只需要直接返回给客户端即可。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if(o instanceof String){
return objectMapper.writeValueAsString(ResultData.success(o));
}
if(o instanceof ResultData){
return o;
}
return ResultData.success(o);
}

关键代码:

1
2
3
java复制代码if(o instanceof ResultData){
return o;
}

如果返回的结果是ResultData对象,直接返回即可。

这时候我们再调用上面的错误方法,返回的结果就符合我们的要求了。

1
2
3
4
5
6
json复制代码{
"status": 500,
"message": "自定义异常",
"data": null,
"timestamp": 1625796580778
}

好了,今天的文章就到这里了,希望通过这篇文章你能掌握如何在你项目中友好实现统一标准格式到返回并且可以优雅的处理全局异常。

老鸟系列源码已经上传至GitHub,需要的在公号【JAVA日知录】回复关键字 0923 获取

本文转载自: 掘金

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

命令模式&中介者模式

发表于 2021-07-20

有情怀,有干货,微信搜索【三太子敖丙】关注这个有一点点东西的程序员。

本文 GitHub github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

最近在跟大家分享设计模式系列的文章有学妹问我,命令模式、策略模式、工厂模式 它们分别有啥区别?看代码的实现上感觉没啥区别呀?

之前已经跟大家分享了策略模式以及工厂模式感兴趣的同学可以再去复习一下,今天我们就先重点分析一下命令模式然后再来看看它们的区别是啥?

往期回顾:

  • 单例模式
  • 工厂模式
  • 流程引擎
  • 建造者模式
  • 原型模式
  • 责任链模式
  • 观察者模式
  • 策略模式
  • 模板方法
  • 迭代器模式
  • 代理模式
  • 对象池&解释器模式

命令模式

定义
  • 提供一个统一的方法来封装命令,通过参数条件来判断选择执行什么命令动作。
  • 允许将每一个命令存储在一个队列中。

整体结构图如下:

结构图中重要角色解释:

  • Command(命令类):定义命令的抽象封装类。
  • ConcreteCommand(具体命令类):对Command类进行实现,说白了就是具体的命令的实际实现类。
  • Receiver(接收者):执行命令关联的操作类。
  • Invoker(调用者):触发命令类,即外部操作事件触发执行。
  • Client(客户端):实例化具体命令对象,及接收者的实际类。

整个结构其实看上去还是比较难理解的,但是既然开始在学设计模式了,那肯定每种设计模式都要有了解,来提升自己的知识面

为了加深理解,我还是举一个好理解的例子:

大家对中国古代君主制度肯定很熟悉。皇帝可以针对手底下服侍的公公让她们可以收取或者发放奏折。那其实这里面我个人感觉就可以体现命令模式。

公公 相当于命令模式的接受者(Receiver),执行皇帝的命令,收取早朝奏折(ConcreteCommand) 还是颁布圣旨(ConcreteCommand)

皇帝 相当于命令模式的调用者(Invoker)

老规矩,例子说完,看看代码吧

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
java复制代码// 定义 命令类
public interface Command {
    // 执行的方法
    void execute();
}

// 定义接收者-公公的角色
public class Receiver {

    public void Charge(){
        System.out.println("收取奏折");
    }

    public void Issue(){
        System.out.println("颁布圣旨");
    }
}


//具体命令类one,收取奏折命令
public class ConcreteCommandOne implements Command {

    // 接受者,这里可以理解为公公
    private Receiver receiver;

    public ConcreteCommandOne(Receiver receiver) {
        this.receiver = receiver;
    }

    @Override
    public void execute() {
        // 收取奏折
        receiver.Charge();
    }
}

// 具体命令类two,颁布圣旨
public class ConcreteCommandTwo implements Command {

    // 接受者,这里可以理解为公公
    private Receiver receiver;

    public ConcreteCommandTwo(Receiver receiver) {
        this.receiver = receiver;
    }

    @Override
    public void execute() {
        // 颁布圣旨
        receiver.Issue();
    }
}

// 调用者,皇帝
public class Invoker {
  
    private Command command;

    public Invoker(Command command) {
        this.command = command;
    }
    // 本次需要执行的命令
    public void action() {
        command.execute();
    }
}

 // 测试demo
    public static void main(String[] args) {
        // 实例化一个公公 接收者
        Receiver receiver =new Receiver();
        // 公公 当前能有接收到的几种命令
        Command commandOne = new ConcreteCommandOne(receiver);
        Command commandTwo = new ConcreteCommandTwo(receiver);

        // 皇帝 发号命令 触发执行方法
        Invoker invoker =new Invoker(commandOne);
        invoker.action();
        // result: 收取奏折

        Invoker invokerTwo =new Invoker(commandTwo);
        invokerTwo.action();
        // result:颁布圣旨
    }

以上就是简单的代码实现了,通过Invoker(皇帝)的选择可以让Receiver(公公)确定去执行什么命令。这其实就是命令模式的一种简单体现。

细心的同学不知道有没有发现一个问题,在定义里面

  • 允许将每一个命令存储在一个队列中。

我们这里是没有体现队列的,其实这个实现也很简单。在main方法中添加一个队列就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码    public static void main(String[] args) {
        // 实例化一个公公 接收者
        Receiver receiver = new Receiver();
        // 公公 当前能有接收到的几种命令
        Command commandOne = new ConcreteCommandOne(receiver);
        Command commandTwo = new ConcreteCommandTwo(receiver);
    // 存储命令
        Queue<Command> queue = new LinkedList<>();
        queue.add(commandOne);
        queue.add(commandTwo);
    // 批量执行
        for (Command command : queue) {
            Invoker invoker = new Invoker(command);
            invoker.action();
        }
    }

这里我想给大家做一个扩展点,这也是我之前看到过一种校验写法。

大家在真实的工作中肯定会遇到很多一些接口的校验,怎么去写这个校验逻辑,怎么做到代码的复用、抽象等这其实是一个比较难的问题!

还是大致的来看下结构图吧!!!

demo代码,我也给大家写出来,需要注意的是我们需要实现 ApplicationContextAware 里面的afterPropertiesSet 方法。

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
java复制代码// 定义抽象校验方法
public abstract class ValidatePlugin {
    public abstract void validate();
}
// 抽象规则执行器
public abstract class ValidatePluginExecute {
    protected abstract List<ValidatePlugin> getValidatePlugins();
    public void execute() {
        final List<ValidatePlugin> validatePlugins = getValidatePlugins();
        if (CollectionUtils.isEmpty(validatePlugins)) {
            return;
        }
        for (ValidatePlugin validatePlugin : validatePlugins) {
          // 执行校验逻辑,这里大家可以根据自己的实际业务场景改造
            validatePlugin.validate();
        }
    }
}

// 具体测试规则
@Component("validatePluginOne")
public class ValidatePluginOne extends  ValidatePlugin {
    @Override
    public void validate() {
        System.out.println("validatePluginOne 规则校验");
    }
}

// 具体执行器,把需要执行的规则添加到 validatePlugins 中
@Component("testValidatePlugin")
public class TestValidatePlugin extends ValidatePluginExecute implements ApplicationContextAware, InitializingBean {

    protected ApplicationContext applicationContext;

    private List<ValidatePlugin> validatePlugins;

    @Override
    public void afterPropertiesSet() {
      // 添加规则
        validatePlugins = Lists.newArrayList();
        validatePlugins.add((ValidatePlugin) this.applicationContext.getBean("validatePluginOne"));

    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    protected List<ValidatePlugin> getValidatePlugins() {
        return this.validatePlugins;
    }
}

// 测试demo
  public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        TestValidatePlugin testValidatePlugin = (TestValidatePlugin) applicationContext.getBean("testValidatePlugin");
        testValidatePlugin.execute();
    }

这个只是一个简单的测试demo,为了让大家有一个思考,设计模式不一定是照搬代码。更多是开拓自己的视野,提升自己解决问题的能力。

针对不同的一些接口,我们只需要在TestValidatePlugin 中添加具体校验规则就可以了,整体的扩展性就变高了,看上去也比较高大上。

所以上面提到的命令模式、策略模式、工厂模式区别是什么呢?

  • 命令模式:属于行为型设计模式,在命令模式中,不同的命令执行过程中会产生不同的目的结果,而且不同的命令是不能替换的。
  • 策略模式 :属于行为型设计模式,在策略模式中,重点在于针对每一种策略执行,解决根据运行时状态从一组策略中选择不同策略的问题
  • 工厂模式:属于创建型设计模式,在工厂模式中,重点在于封装对象的创建过程,这里的对象没有任何业务场景的限定,可以是策略,但也可以是其他东西

所以针对设计模式,其实我理解的还是只说明了一个问题,不同的设计模式都是为了针对处理不同的场景,不同业务场景有不同的写法。

中介者模式

中介者模式,看这个名字也能理解出来,定一个中间结构来方便管理下游组织。

那么什么是中介模式呢?

在GoF 中的《设计模式》中解释为:中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。

再来看看这个结构图吧:

  • Mediator(抽象中介者):用来定义参与者与中介者之间的交互方式
  • ConcreteMediator(具体中介者):实现中介者定义的操作,即就是实现交互方式。
  • Colleague(抽象同事角色):抽象类或者接口,主要用来定义参与者如何进行交互。
  • ConcreteColleague(具有同事角色):很简单,就是具体的实现Colleague中的方法。

以上结构定义来自设计模式之美

看这个结构图理解出来,其实是跟之前为大家写的一篇观察者模式有点相同的,感兴趣的同学可以再去复习一下。

老规矩,还是具体举例代码实现一下

高铁系统大家应该清楚有一个调度中心,用来控制每一辆高铁的进站顺序,如果没有这个调度中心,当同时有三量高铁都即将进站时,那他们就需要两两相护沟通。假设有其中的一辆动车没有沟通到,那就将发生不可估量的错误,所以就需要通过这个调度中心来处理这个通信逻辑,同时来管理当前有多少车辆等待进站等。

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
java复制代码// 抽象参与者, 也可以使用abstract 写法
public interface Colleague {
   // 沟通消息
    void message();
}
// 抽象中介者
public interface Mediator {
    // 定义处理逻辑
    void doEvent(Colleague colleague);
}

// 具体参与者
@Component
public class MotorCarOneColleague implements Colleague {

    @Override
    public void message() {
        // 模拟处理业务逻辑
        System.out.println("高铁一号收到消息!!!");
    }
}
@Component
public class MotorCarTwoColleague implements Colleague {
    @Override
    public void message() {
        System.out.println("高铁二号收到消息!!!");
    }
}
@Component
public class MotorCarThreeColleague implements Colleague {
    @Override
    public void message() {
        System.out.println("高铁三号收到消息!!!");
    }
}

// 具体中介者
@Component
public class DispatchCenter implements Mediator {
  // 管理有哪些参与者
    @Autowired
    private List<Colleague> colleagues;
  
    @Override
    public void doEvent(Colleague colleague) {
        for(Colleague colleague1 :colleagues){
            if(colleague1==colleague){
                // 如果是本身高铁信息,可以处理其他的业务逻辑
                // doSomeThing();
                continue;
            }
          // 通知其他参与
            colleague1.message();
        }
    }
}

// 测试demo
public static void main(String[] args) {
     // 初始化spring容器
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
     // 获取中介者,调度中心
        DispatchCenter dispatchCenter = (DispatchCenter) applicationContext.getBean("dispatchCenter");


        // 一号高铁 发送消息出去
        MotorCarOneColleague motorCarOneColleague =  (MotorCarOneColleague) applicationContext.getBean("motorCarOneColleague");
     // 通过调度中心沟通信息
        dispatchCenter.doEvent(motorCarOneColleague);
        // result:高铁三号收到消息!!!
        //         高铁二号收到消息!!!


        // 二号高铁 发送消息出去
        MotorCarTwoColleague  motorCarTwoColleague = (MotorCarTwoColleague)applicationContext.getBean("motorCarTwoColleague");
        dispatchCenter.doEvent(motorCarTwoColleague);
        // result:高铁一号收到消息!!!
        //         高铁三号收到消息!!!

    }

中介者模式demo代码就算完成了,通过这个demo大家应该能发现,中介者还是很好理解的。

但是中介者的应用场景还是比较少见的,针对一些类依赖严重,形成的类似网状结构,改成一个类似与蒲公英一样结构,由中间向外扩散,来达到解耦合的效果。

更多在一个UI界面控件里面比较常见,当然在Java里面java.util.Timer 也可以理解为中介者模式,因为它能控制内部线程如何去运行比如多久运行一次等。

上面提到中介者和观察者模式很像,通过demo代码大家也能发现这一点

观察者模式中观察者和被观察者我们基本时固定的,而中介者模式中,观察者和被观察者时不固定的,而且中介者可能会最后变成一个庞大的原始类。

总结

命令模式:虽然不怎么常见,但是我们还是要区分它与工厂模式以及策略模式的区别是啥,应用场景是啥,能给我们带来什么思考。

比如我最后的那个例子,命令模式可以实现命令的存储,本质是将命令维护在一个队列中,那么在我们的业务代码中 我们为什么不能也通过一个数组来维护一些接口校验依赖,里面存放需要校验的bean实例。来提高代码的复用性以及扩展性。

中介模式:整体来说这个更加不怎么应用,虽然能起到对象的解耦合,但是也有副作用,而且在我们的真实业务场景中也很少会遇到这样的场景,了解一下实现原理即可,至于与观察者的区别,上面也有讲到,更多我们可能是已经在使用一些中间件消息队列去处理了。

我是敖丙,你知道的越多,你不知道的越多,感谢各位人才的:点赞、收藏和评论,我们下期见!


文章持续更新,可以微信搜一搜「 三太子敖丙 」第一时间阅读,回复【资料】有我准备的一线大厂面试资料和简历模板,本文 GitHub github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。

本文转载自: 掘金

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

Java 中节省 90% 时间的常用的工具类

发表于 2021-07-20

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

前言

你们有木有喜欢看代码的领导啊,我的领导就喜欢看我写的代码,有事没事就喜欢跟我探讨怎么写才最好,哈哈哈…挺好。

今天我们就一起来看看可以节省 90% 的加班时间的第三方开源库吧,第一个介绍的必须是 Apache 下的 Commons 库。第二个是 google 开源的 Guava 库。

Apache Commons

Apache Commons 是一个功能非常强大、经常被使用到的库。它有 40 个左右的类库,包含了对字符串、日期、数组等的操作。

Lang3

Lang3 是一个处理 Java 中基本对象的包,比如用 StringUtils 类操作字符串、ArrayUtils 类操作数组、DateUtils 类可以处理日期、MutablePair 类可以返回多个字段等等。

包结构:

image-20210719140346416

maven 依赖

1
2
3
4
5
java复制代码<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>

字符串操作

对字符串快速操作,在 if else 的少写判空条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public static void main(String[] args) {
boolean blank = StringUtils.isBlank(" ");//注意此处是null哦 这和isEmpty不一样的
System.out.println(blank);

boolean empty = StringUtils.isEmpty(" ");//注意这里是false
System.out.println(empty);

boolean anyBlank = StringUtils.isAnyBlank("a", " ", "c");// 其中一个是不是空字符串
System.out.println(anyBlank);

boolean numeric = StringUtils.isNumeric("1");//字符串是不是全是数字组成,"." 不算数字
System.out.println(numeric);

String remove = StringUtils.remove("abcdefgh", "a");//移除字符串
System.out.println(remove);
}

输出结果:

1
2
3
4
5
6
7
vbnet复制代码true
false
true
true
bcdefgh

Process finished with exit code 0

日期操作

终于可以不用 SimpleDateFormat 格式化日期了,DateUtils.iterator 可以获取一段时间。

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 static void main(String[] args) throws ParseException {

Date date = DateUtils.parseDate("2021-07-15", "yyyy-MM-dd");

Date date1 = DateUtils.addDays(date, 1);//加一天
System.out.println(date1);

boolean sameDay = DateUtils.isSameDay(date, new Date());//比较
System.out.println(sameDay);
/*
获取一段日期
RANGE_WEEK_SUNDAY 从周日开始获取一周日期
RANGE_WEEK_MONDAY 从周一开始获取一周日期
RANGE_WEEK_RELATIVE 从当前时间开始获取一周日期
RANGE_WEEK_CENTER 以当前日期为中心获取一周日期
RANGE_MONTH_SUNDAY 从周日开始获取一个月日期
RANGE_MONTH_MONDAY 从周一开始获取一个月日期
*/
Iterator<Calendar> iterator = DateUtils.iterator(date, DateUtils.RANGE_WEEK_CENTER);
while (iterator.hasNext()) {
Calendar next = iterator.next();
System.out.println(DateFormatUtils.format(next, "yyyy-MM-dd"));
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码Fri Jul 16 00:00:00 CST 2021
false
2021-07-12
2021-07-13
2021-07-14
2021-07-15
2021-07-16
2021-07-17
2021-07-18

Process finished with exit code 0

返回多个字段

有时候在一个方法中需要返回多个值的时候,经常会使用 HashMap 返回或者是 JSON 返回。Lang3 下已经帮我们提供了这样的工具类,不需要再多写 HashMap 和 JSON 了。

1
2
3
4
5
6
7
8
java复制代码public static void main(String[] args) {

MutablePair<Integer, String> mutablePair = MutablePair.of(2, "这是两个值");
System.out.println(mutablePair.getLeft() + " " + mutablePair.getRight());

MutableTriple<Integer, String, Date> mutableTriple = MutableTriple.of(2, "这是三个值", new Date());
System.out.println(mutableTriple.getLeft() + " " + mutableTriple.getMiddle() + " " + mutableTriple.getRight());
}

输出结果:

1
2
3
4
vbnet复制代码2  这是两个值
2 这是三个值 Fri Jul 16 15:24:40 CST 2021

Process finished with exit code 0

ArrayUtils 数组操作

ArrayUtils 是专门处理数组的类,可以让方便的处理数组而不是需要各种循环操作。

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
java复制代码public static void main(String[] args) {

//合并数组
String[] array1 = new String[]{"value1", "value2"};
String[] array2 = new String[]{"value3", "value4"};
String[] array3 = ArrayUtils.addAll(array1, array2);
System.out.println("array3:"+ArrayUtils.toString(array3));

//clone 数组
String[] array4 = ArrayUtils.clone(array3);
System.out.println("array4:"+ArrayUtils.toString(array4));

//数组是否相同
boolean b = EqualsBuilder.reflectionEquals(array3, array4);
System.out.println(b);

//反转数组
ArrayUtils.reverse(array4);
System.out.println("array4反转后:"+ArrayUtils.toString(array4));

//二维数组转 map
Map<String, String> arrayMap = (HashMap) ArrayUtils.toMap(new String[][]{
{"key1", "value1"}, {"key2", "value2"}
});
for (String s : arrayMap.keySet()) {
System.out.println(arrayMap.get(s));
}
}

输出结果:

1
2
3
4
5
6
7
8
vbnet复制代码array3:{value1,value2,value3,value4}
array4:{value1,value2,value3,value4}
true
array4反转后:{value4,value3,value2,value1}
value1
value2

Process finished with exit code 0

EnumUtils 枚举操作

  • getEnum(Class enumClass, String enumName) 通过类返回一个枚举,可能返回空;
  • getEnumList(Class enumClass) 通过类返回一个枚举集合;
  • getEnumMap(Class enumClass) 通过类返回一个枚举map;
  • isValidEnum(Class enumClass, String enumName) 验证enumName是否在枚举中,返回true或false。
1
2
3
java复制代码public enum ImagesTypeEnum {
JPG,JPEG,PNG,GIF;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码    public static void main(String[] args) {
ImagesTypeEnum imagesTypeEnum = EnumUtils.getEnum(ImagesTypeEnum.class, "JPG");
System.out.println("imagesTypeEnum = " + imagesTypeEnum);
System.out.println("--------------");
List<ImagesTypeEnum> imagesTypeEnumList = EnumUtils.getEnumList(ImagesTypeEnum.class);
imagesTypeEnumList.stream().forEach(
imagesTypeEnum1 -> System.out.println("imagesTypeEnum1 = " + imagesTypeEnum1)
);
System.out.println("--------------");
Map<String, ImagesTypeEnum> imagesTypeEnumMap = EnumUtils.getEnumMap(ImagesTypeEnum.class);
imagesTypeEnumMap.forEach((k, v) -> System.out.println("key:" + k + ",value:" + v));
System.out.println("-------------");
boolean result = EnumUtils.isValidEnum(ImagesTypeEnum.class, "JPG");
System.out.println("result = " + result);
boolean result1 = EnumUtils.isValidEnum(ImagesTypeEnum.class, null);
System.out.println("result1 = " + result1);
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码imagesTypeEnum = JPG
--------------
imagesTypeEnum1 = JPG
imagesTypeEnum1 = JPEG
imagesTypeEnum1 = PNG
imagesTypeEnum1 = GIF
--------------
key:JPG,value:JPG
key:JPEG,value:JPEG
key:PNG,value:PNG
key:GIF,value:GIF
-------------
result = true
result1 = false

Process finished with exit code 0

collections4 集合操作

commons-collections4 增强了 Java 集合框架,提供了一系列简单的 API 方便操作集合。

maven 依赖

1
2
3
4
5
java复制代码 <dependency>  
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>

CollectionUtils 工具类

这是一个工具类,可以检查 null 元素不被加入集合,合并列表,过滤列表,两个列表的并集、差集、合集。有部分功能在 Java 8 中可以被 Stream 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
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码public static void main(String[] args) {

//null 元素不能加进去
List<String> arrayList1 = new ArrayList<>();
arrayList1.add("a");
CollectionUtils.addIgnoreNull(arrayList1, null);
System.out.println(arrayList1.size());

//排好序的集合,合并后还是排序的
List<String> arrayList2 = new ArrayList<>();
arrayList2.add("a");
arrayList2.add("b");

List<String> arrayList3 = new ArrayList<>();
arrayList3.add("c");
arrayList3.add("d");
System.out.println("arrayList3:" + arrayList3);

List<String> arrayList4 = CollectionUtils.collate(arrayList2, arrayList3);
System.out.println("arrayList4:" + arrayList4);

//交集
Collection<String> strings = CollectionUtils.retainAll(arrayList4, arrayList3);
System.out.println("arrayList3和arrayList4的交集:" + strings);

//并集
Collection<String> union = CollectionUtils.union(arrayList4, arrayList3);
System.out.println("arrayList3和arrayList4的并集:" + union);

//差集
Collection<String> subtract = CollectionUtils.subtract(arrayList4, arrayList3);
System.out.println("arrayList3和arrayList4的差集:" + subtract);

// 过滤,只保留 b
CollectionUtils.filter(arrayList4, s -> s.equals("b"));
System.out.println(arrayList4);
}

输出结果:

1
2
3
4
5
6
7
8
9
less复制代码1
arrayList3:[c, d]
arrayList4:[a, b, c, d]
arrayList3和arrayList4的交集:[c, d]
arrayList3和arrayList4的并集:[a, b, c, d]
arrayList3和arrayList4的差集:[a, b]
[b]

Process finished with exit code 0

Bag 统计次数

用于统计值在集合中出现的次数。

1
2
3
4
5
6
7
8
9
java复制代码public static void main(String[] args) {
Bag bag = new HashBag<String>();
bag.add("a");
bag.add("b");
bag.add("a");
bag.add("c", 3);
System.out.println(bag);
System.out.println(bag.getCount("c"));
}

输出结果:

1
2
3
4
ruby复制代码[2:a,1:b,3:c]
3

Process finished with exit code 0

beanutils Bean 操作

beanutils 是通过反射机制对 JavaBean 进行操作的。比如对 Bean 进行复制、map 转对象、对象转 Map。

maven 依赖

1
2
3
4
5
java复制代码<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class User {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public static void main(String[] args) throws Exception {
User user1 = new User();
user1.setName("李四");
User user2 = (User) BeanUtils.cloneBean(user1);
System.out.println(user2.getName());

//User 转 map
Map<String, String> describe = BeanUtils.describe(user1);
System.out.println(describe);

//Map 转 User
Map<String, String> beanMap = new HashMap();
beanMap.put("name", "张三");
User user3 = new User();
BeanUtils.populate(user3, beanMap);
System.out.println(user3.getName());
}

输出结果:

1
2
3
4
5
ini复制代码李四
{name=李四}
张三

Process finished with exit code 0

Guava

Google 开源的一个基于 Java 扩展项目,包含了一些基本工具、集合扩展、缓存、并发工具包、字符串处理等。

maven 依赖

1
2
3
4
5
java复制代码<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>

Map<String, List> 类型

在java 代码中经常会遇到需要写 Map<String, List> map 的局部变量的时候。有时候业务情况还会更复杂一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public static void main(String[] args) {
//以前
Map<String, List<String>> map = new HashMap<>();
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
map.put("名称", list);
System.out.println(map.get("名称"));

//现在
Multimap<String, String> multimap = ArrayListMultimap.create();
multimap.put("名称", "张三");
multimap.put("名称", "李四");
System.out.println(multimap.get("名称"));
}

输出结果:

1
2
3
4
css复制代码[张三, 李四]
[张三, 李四]

Process finished with exit code 0

value 不能重复的 Map

在 Map 中 value 的值时可以重复的,Guava 可以创建一个 value 不可重复的 Map,并且 Map 和 value 可以对调。

1
2
3
4
5
6
7
java复制代码public static void main(String[] args) {
//会报异常
BiMap<String ,String> biMap = HashBiMap.create();
biMap.put("key1", "value");
biMap.put("key2", "value");
System.out.println(biMap.get("key1"));
}

输出结果:

1
2
3
4
5
6
php复制代码Exception in thread "main" java.lang.IllegalArgumentException: value already present: value
at com.google.common.collect.HashBiMap.put(HashBiMap.java:287)
at com.google.common.collect.HashBiMap.put(HashBiMap.java:262)
at org.example.clone.Test.main(Test.java:17)

Process finished with exit code 1
1
2
3
4
5
6
7
8
9
10
java复制代码public static void main(String[] args) {
BiMap<String ,String> biMap = HashBiMap.create();
biMap.put("key1", "value1");
biMap.put("key2", "value2");
System.out.println(biMap.get("key1"));

//key-value 对调
biMap = biMap.inverse();
System.out.println(biMap.get("value1"));
}

输出结果:

1
2
3
4
vbnet复制代码value1
key1

Process finished with exit code 0

Guava cache

写业务的时候肯定会使用缓存,当不想用第三方作为缓存的时候,Map 又不够强大,就可以使用 Guava 的缓存。

缓存的并发级别

Guava提供了设置并发级别的API,使得缓存支持并发的写入和读取。与ConcurrentHashMap类似,Guava cache的并发也是通过分离锁实现。在通常情况下,推荐将并发级别设置为服务器cpu核心数。

1
2
3
4
java复制代码CacheBuilder.newBuilder()
// 设置并发级别为cpu核心数,默认为4
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
.build();

缓存的初始容量设置

我们在构建缓存时可以为缓存设置一个合理大小初始容量,由于Guava的缓存使用了分离锁的机制,扩容的代价非常昂贵。所以合理的初始容量能够减少缓存容器的扩容次数。

1
2
3
4
java复制代码CacheBuilder.newBuilder()
// 设置初始容量为100
.initialCapacity(100)
.build();

设置最大存储

Guava Cache可以在构建缓存对象时指定缓存所能够存储的最大记录数量。当Cache中的记录数量达到最大值后再调用put方法向其中添加对象,Guava会先从当前缓存的对象记录中选择一条删除掉,腾出空间后再将新的对象存储到Cache中。

1
2
3
4
5
6
7
java复制代码public static void main(String[] args) {
Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(2).build();
cache.put("key1", "value1");
cache.put("key2", "value2");
cache.put("key3", "value3");
System.out.println(cache.getIfPresent("key1")); //key1 = null
}

输出结果:

1
2
3
vbscript复制代码null

Process finished with exit code 0

过期时间

expireAfterAccess() 可以设置缓存的过期时间。

1
2
3
4
5
6
7
8
9
java复制代码public static void main(String[] args) throws InterruptedException {
//设置过期时间为2秒
Cache<String, String> cache1 = CacheBuilder.newBuilder().maximumSize(2).expireAfterAccess(2, TimeUnit.SECONDS).build();
cache1.put("key1", "value1");
Thread.sleep(1000);
System.out.println(cache1.getIfPresent("key1"));
Thread.sleep(2000);
System.out.println(cache1.getIfPresent("key1"));
}

输出结果:

1
2
3
4
vbscript复制代码value1
null

Process finished with exit code 0

LoadingCache

使用自定义ClassLoader加载数据,置入内存中。从LoadingCache中获取数据时,若数据存在则直接返回;若数据不存在,则根据ClassLoader的load方法加载数据至内存,然后返回该数据。

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

public static void main(String[] args) throws Exception {
System.out.println(numCache.get(1));
Thread.sleep(1000);
System.out.println(numCache.get(1));
Thread.sleep(1000);
numCache.put(1, 6);
System.out.println(numCache.get(1));

}

private static LoadingCache<Integer, Integer> numCache = CacheBuilder.newBuilder().
expireAfterWrite(5L, TimeUnit.MINUTES).
maximumSize(5000L).
build(new CacheLoader<Integer, Integer>() {
@Override
public Integer load(Integer key) throws Exception {
System.out.println("no cache");
return key * 5;
}
});
}

输出结果:

1
2
3
4
5
6
java复制代码no cache
5
5
6

Process finished with exit code 0

总结

通过 Apache Commons 和 Guava 两个第三方的开源工具库,可以减少循环、ifelse 的代码。写出的代码更有健壮性并且可以在新人面前装一波。Apache Commons 和 Guava 有许许多多的工具类,这里只列出了小小的部分,可以在官网例子中查看到各种用法。

最后

我是一个正在被打击还在努力前进的码农。如果文章对你有帮助,记得点赞、关注哟,谢谢!

本文转载自: 掘金

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

AI插件Github Copilot使用及用它解LeetCo

发表于 2021-07-19

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

之前提交的github copilot技术预览版申请,今天收到准入邮件,于是安上试一试这个准备把我送去电子厂上班的copy a lot ?

官网及申请地址:copilot.github.com/


小作文包含如下内容:

  • copilot简单介绍
  • 使用python对copilot做些简单使用测试
  • 使用copilot对LeetCode 题目解答

一、copilot简单介绍

image-20210719145925929

github copilot(副驾驶)目前只适用于vscode的扩展插件,它依赖于github数十亿公开代码库的训练而成的AI编码辅助器(包括整行代码提供或函数建议),目前支持数十种编程语言,技术预览版对 Python、JavaScript、TypeScript、Ruby 和 Go 的表现尤其出色。

他的工作原理:通过大量公共代码库对AI模型训练后构建成copilot服务,服务接收来自copilot插件返回的提要编码,并提供代码建议,插件又将来自程序员对建议的采纳性的回传到copilot服务,如此反复强化AI模型。

下图以蔽之:

image-20210719150443888


二、使用python对copilot做些简单使用测试

在vscode插件中我们安上Github Copilot,建一个测试文件Copilot_test.py

image-20210719152837439

1.获取列表的最大和最小值的函数

我们要写的可能看起来是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码'''
Function to get the max and min values of a list
'''
def get_max_min(my_list):
   max_value = max(my_list)
   min_value = min(my_list)
   return max_value, min_value
​
def main():
   my_list = [1,2,3,4,5]
   max_value, min_value = get_max_min(my_list)
   print("Max value:", max_value)
   print("Min value:", min_value)
   
if __name__ == "__main__":
   main()
​

注释是copilot的关键部分,所有AI的是基于大数据的应用,甚至可以把copilot简单的认为是对github代码库的检索…

max_min

2.一个计算器

calculator

copilot给出的建议允许我们进行选择,通过Alt+[,Alt+]对建议上下查看。

image-20210719164153638

我们可以使用Ctrl+Eeter打开建议结果面板,可以看到对应这些建议,copilot给了我们是10个解决方案

image-20210719164636671


三、使用copilot对LeetCode 题目解答

我们在LeetCode找一题【回文数】,题目如下:

然后我们把题目写到代码注释中

image-20210719165443857

代码区的类也加过来

image-20210719165614176

看起来我们的代码就是这样的,灰色code的copilot给出的建议

image-20210719165941775

我们选择其中一种建议放到LeetCode的执行看看

image-20210719170230672

image-20210719170355312

这个建议似乎不太理想…勇敢牛牛不怕困难,有兴趣的同学可以看看困难模式的题目哦,PS:对于中文的注释不确定copilot能百分之百给出回应。


copilot就像它的名字一样【副驾驶】,正经事还是得你来干,不过未来可能会一个不错的协助工具。


文章有不足的地方欢迎在评论区指出。

欢迎收藏、点赞、提问。关注顶级饮水机管理员,除了管烧热水,有时还做点别的。

本文转载自: 掘金

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

头歌-Mongo入门

发表于 2021-07-19

第一章 MongoDB数据库增删改查

1-2 Mongodb 数据库基本操作

第三关 文档操作一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use Testdb3   // 进入 Testdb3数据库
document = {
_id: 1,
name: "张小华",
sex: "男",
phone: "12356986594",
hobbies: ["打篮球", "踢足球", "唱歌"]
};
db.stu1.insert(document);
db.stu2.insert(document);
db.stu3.insert(document);
db.stu2.update({ phone: "12356986594" }, { $set: { phone: "18356971462" } });
db.stu2.find().pretty(); // 查看stu2
db.stu3.save({
_id: 1,
name: "张晓晓",
sex: "女",
phone: "12365498704",
hobbies: ["跳舞", "羽毛球", "唱歌"],
});

第四关 文档操作二

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
#########begin#########
echo '
document = [
{
"_id" : 1,
"name" : "西西",
"sex" : "女",
"age" : 23,
"national" : "汉族"
},
{
"_id" : 2,
"name" : "东东",
"sex" : "男",
"age" : 20,
"national" : "苗族"
},
{
"_id" : 3,
"name" : "北北",
"sex" : "男",
"age" : 19,
"national" : "汉族"
},
{
"_id" : 4,
"name" : "南南",
"sex" : "女",
"age" : 15,
"national" : "傣族"
}
];

db.stu1.insert(document);
db.stu2.insert(document);
db.stu1.find({ age:{$gte: 15 } ,sex: "女" });
db.stu1.find({ national:"苗族"}).pretty();
db.stu1.find({ age:{$lt: 20 }, sex: "男" });
db.stu2.remove({})
'
#########end#########

第二章 MongoDB数据库的权限设置

2-1 MongoDB数据库安全

第一关 创建管理员用户

1
2
use admin
db.createUser({user:"admin",pwd:"123456",roles:[{role:"root",db:"admin"}]})

第二关 按需求创建普通用户

1
2
use firstdb 
db.createUser({user:"people",pwd:"people",roles:[{role:"read",db:"firstdb"}]})

第三关 数据库限制访问

1
2
3
4
use admin #进入admin数据库
db.shutdownServer() #关闭服务
exit #退出数据库
mongod -port 20018 --dbpath /data/db --logpath /tmp/mongodb.log --bind_ip 127.0.0.1 --fork

第三章 MongoDB数据库高级查询

3-1 MongoDB 之聚合函数查询统计

第一关 聚合管道操作符将文档定制格式输出(一)

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
# 先将数据插入
use test1
document = [
{
"_id" : 1,
"course" : "Python表达式问题求解实训",
"author" : "李暾",
"tags" : [
"Python基础",
"求解"
],
"learning_num" : 1882
},
{
"_id" : 2,
"course" : "Java语言之基本语法",
"author" : "余跃",
"tags" : [
"Java基础",
"语法"
],
"learning_num" : 814
},
{
"_id" : 3,
"course" : "Python面向对象编程实训",
"author" : "李暾",
"tags" : [
"Python基础",
"面向对象"
],
"learning_num" : 143
},
{
"_id" : 4,
"course" : "Android综合实训之物联网移动应用开发(1)",
"author" : "prophet5",
"tags" : [
"Android",
"物联网",
"移动开发"
],
"learning_num" : 207
}
]
db.educoder.insert(document)

#********* Begin *********#
echo "
db.educoder.aggregate({\$project:{_id:0,course:1,learning_num:1}});
db.educoder.aggregate({\$match:{learning_num:1882}});
"
#********* End *********#

第二关 聚合管道操作符将文档定制格式输出(二)

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
# 先插入数据
use test2
document = [
{
"_id" : 1,
"course" : "Python表达式问题求解实训",
"author" : "李暾",
"tags" : [
"Python基础",
"求解"
],
"learning_num" : 1882
},
{
"_id" : 2,
"course" : "Java语言之基本语法",
"author" : "余跃",
"tags" : [
"Java基础",
"语法"
],
"learning_num" : 814
},
{
"_id" : 3,
"course" : "Python面向对象编程实训",
"author" : "李暾",
"tags" : [
"Python基础",
"面向对象"
],
"learning_num" : 143
},
{
"_id" : 4,
"course" : "Android综合实训之物联网移动应用开发(1)",
"author" : "prophet5",
"tags" : [
"Android",
"物联网",
"移动开发"
],
"learning_num" : 207
}
]
db.educoder.insert(document)

#********* Begin *********#
echo "

db.educoder.aggregate({\$limit:3});
db.educoder.aggregate({\$sort:{learning_num:1}});
db.educoder.aggregate([{\$skip:2}]);

"
#********* End *********#

第三关 聚合表达式对文档数据进行统计

1
2
3
4
5
6
7
8
# 先插入数据 (自行插入)
#********* Begin *********#
echo "
db.educoder.aggregate([{\$group:{_id:'\$author',first_course:{\$first:'\$course'}}}]);
db.educoder.aggregate([{\$group:{_id:'\$author',learning_avg:{\$avg:'\$learning_num'}}}]);
db.educoder.aggregate([{ \$unwind:'\$tags'} , { \$group:{_id:'\$tags',course_num:{\$sum:1} } }]
"
#********* End *********#

3-2 MongoDB 之滴滴、摩拜都在用的索引

第一关 了解并创建一个简单索引

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
use test 
document = [
{
"_id" : "1",
"name" : "王小明",
"age" : "15",
"score" : "90"
},
{
"_id" : "2",
"name" : "周晓晓",
"age" : "18",
"score" : "86"
},
{
"_id" : "3",
"name" : "王敏",
"age" : "20",
"score" : "96"
},
{
"_id" : "4",
"name" : "李晓亮",
"age" : "15",
"score" : "74"
},
{
"_id" : "5",
"name" : "张青青",
"age" : "21",
"score" : "88"
}
]
db.student.insert(document)
db.student.createIndex({score:-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
document = [
{
"_id" : "1",
"title" : "提升程序员工作效率的6个工具利器",
"tags" : "Alfred,幕布",
"follwers" : 543
},
{
"_id" : "2",
"title" : "我是如何从零开始学习前端的",
"tags" : "HTML,Html5,CSS",
"follwers" : 1570
},
{
"_id" : "3",
"title" : "20个非常有用的JAVA程序片段",
"tags" : "Java,编程",
"follwers" : 1920
}
]
use test2
db.article.insert(document)
# 用字段 follwers 和 title 创建复合升序索引;
db.article.createIndex({ follwers: 1, title: 1 });
# 用字段 tags 创建多 key 降序索引;
db.article.createIndex({ tags: -1 });
# 用_id创建哈希索引;
db.article.createIndex({ _id: "hashed" });
# 用字段 title 和 tags 创建文本索引。
db.article.createIndex(
... {
... title:"text",
... tags:"text"
... }
... )

第三关 有NM趣的地理位置索引

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
#**********Begin**********#
echo '
db.people.insert({
_id: 1,
name: "A",
personloc: { type: "Point", coordinates: [116.403981, 39.914935] },
});

db.people.insert({
_id: 2,
name: "B",
personloc: { type: "Point", coordinates: [116.433733, 39.909511] },
});

db.people.insert({
_id: 3,
name: "C",
personloc: { type: "Point", coordinates: [116.488781, 39.949901] },
});

db.people.insert({
_id: 4,
name: "D",
personloc: { type: "Point", coordinates: [116.342609, 39.948021] },
});

db.people.insert({
_id: 5,
name: "E",
personloc: { type: "Point", coordinates: [116.328236, 39.901098] },
});

db.people.insert({
_id: 6,
name: "F",
personloc: { type: "Point", coordinates: [116.385728, 39.871645] },
});

db.people.createIndex({ personloc: "2dsphere" });

db.runCommand({
geoNear: "people",
near: { type: "Point", coordinates: [116.403981, 39.914935] },
spherical: true,
minDistance: 100,
maxDistance: 3000,
});
db.runCommand({
geoNear: "people",
near: { type: "Point", coordinates: [116.433733, 39.909511] },
spherical: true,
minDistance: 100,
maxDistance: 5000,
});
db.runCommand({
geoNear: "people",
near: { type: "Point", coordinates: [116.488781, 39.949901] },
spherical: true,
minDistance: 3000,
maxDistance: 8000,
});
db.runCommand({
geoNear: "people",
near: { type: "Point", coordinates: [116.342609, 39.948021] },
spherical: true,
minDistance: 3000,
maxDistance: 8000,
});
'
#**********End**********#

3-3 MondoDB文档的高级查询操作

第一关 数据的导入导出

1
2
3
4
5
6
7
8
#  將 /home/example 路径下的文件 student.csv 导入到数据库 mydb1 的 test 集合中;
mongoimport -d mydb1 -c test --type csv --headerline --ignoreBlanks --file /home/example/student.csv
# 将 /home/example/person.json 文件导入到数据库 mydb2 中的 test 集合中。
mongoimport -d mydb2 -c test --type json --file /home/example/person.json
# 将数据库 mydb1 的 test 集合以 json 格式导出到 /home/test1.json 的 json 文件中;
mongoexport -d mydb1 -c test -o /home/test1.json --type json
# 将数据库 mydb1 的 test 集合以 csv 格式导出到 /home/test1.csv 的 CSV 文件中
mongoexport -d mydb1 -c test -o /home/test1.csv --type csv -f "_id,name,age,sex,major"

第二关 高级查询(一)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 执行查询命令,查找所有喜欢唱歌和跳舞的人的信息,并按照_id升序排序;
db.test.find({ hobbies: { $all: ["唱歌", "跳舞"] } }).sort({ _id: 1 });
// 执行查询命令,查找所有喜欢羽毛球和跳舞的人的信息,并按照_id升序排序;
db.test.find({ hobbies: { $all: ["羽毛球", "跳舞"] } }).sort({ _id: 1 });
// 执行查询命令,查找有3个爱好的人的信息,并按照_id升序排序;
db.test.find({ hobbies: { $size: 3 } }).sort({ _id: 1 });
// 执行查询命令,查找文档中存在 hobbies 字段的人的信息,并按照_id升序排序;
db.test.find({ hobbies: { $exists: true } }).sort({ _id: 1 });
// 执行查询命令,查找19岁和23岁的人的信息,并按照_id升序排序;
db.test.find({ age: { $in: [19, 23] } }).sort({ _id: 1 });
// 执行查询命令,查找不是20岁的人的信息,并按照_id升序排序;
db.test.find({ age: { $nin: [20] } }).sort({ _id: 1 });
// 执行查询命令,查找 age 取模9等于2的人的信息,并按照_id升序排序。
db.test.find({ age: { $mod: [9, 2] } }).sort({ _id: 1 });
1
2
3
4
5
6
7
8
9
10
11
#********* Begin *********#
echo "
db.test.find({ hobbies: { \$all: ['唱歌', '跳舞'] } }).sort({ _id: 1 });
db.test.find({ hobbies: { \$all: ['羽毛球', '跳舞'] } }).sort({ _id: 1 });
db.test.find({ hobbies: { \$size: 3 } }).sort({ _id: 1 });
db.test.find({ hobbies: { \$exists: true } }).sort({ _id: 1 });
db.test.find({ age: { \$in: [19, 23] } }).sort({ _id: 1 });
db.test.find({ age: { \$nin: [20] } }).sort({ _id: 1 });
db.test.find({ age: { \$mod: [9, 2] } }).sort({ _id: 1 });
"
#********* End *********#

第三关 高级查询(二)

1
2
3
4
5
6
7
8
9
10
11
12
13
mongoimport -d mydb3 -c test --type json --file /home/example/person.json
#********* Begin *********#
echo "
db.test.find({\$and:[{age:20},{sex:'男'}]}).sort({_id:1});
db.test.find({\$or:[{age:20},{sex:'男'}]}).sort({_id:1});
db.test.find({name:/^韩./}).sort({_id:1});
db.test.find({\$and:[{age:{\$gte:19}},{age:{\$lt:22}}]}).sort({_id:1});
db.test.find({\$or:[{age:{\$lt:19}},{age:{\$gt:21}}]}).sort({_id:1});
db.test.find({name:{\$not:/^韩./}}).sort({_id:1});
db.test.find({name:{\$not:/^韩.*/}}).sort({_id:1}).count();
db.test.find({\$and:[{age:{\$gte:19}},{age:{\$lt:22}}]}).sort({_id:1}).count();
"
#********* End *********#

第四关 游标

1
2
3
4
use mydb4
for(var i=0;i<10000;i++)db.test.insert({_id:i,title:"MongoDB"+i,content:"hello"+i})

mongoexport -d mydb4 -c test -o /home/test/test4.csv --type csv -f "_id,title,content"

第四章 MongoDB分布式集群

4-1 Mongodb复制集&分片

第一关 ACD ABD

第二关 MongoDB 复制集搭建

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
mkdir -p /data/test/db1 /data/test/db2 /data/test/db3
mkdir -p /logs/test
touch { /logs/test/mongod1.log /logs/test/mongod2.log /logs/test/mongod3.log }
mkdir -p /etc/test
touch { /etc/test/mongod1.conf /etc/test/mongod2.conf /etc/test/mongod3.conf }

# 创建三个配置文件
port=20001 #配置端口号
dbpath=/data/test/db1 #配置数据存放的位置
logpath=/logs/test/mongod1.log #配置日志存放的位置
logappend=true #日志使用追加的方式
fork=true #设置在后台运行
replSet=CHANG #配置复制集名称,该名称要在所有的服务器一致

mongod -f /etc/test/mongod1.conf
mongod -f /etc/test/mongod2.conf
mongod -f /etc/test/mongod3.conf

config = {
_id:"CHANG",
members:[
{_id:0,host:'127.0.0.1:20001'},
{_id:1,host:'127.0.0.1:20002',arbiterOnly:true},
{_id:2,host:'127.0.0.1:20003'},
]
}
rs.initiate(config)

image.png

第三关 MongoDB分片集搭建

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
mkdir -p /data/test2/shard1/db
mkdir -p /logs/test2/shard1/log
mkdir -p /data/test2/shard2/db
mkdir -p /logs/test2/shard2/log
mkdir -p /data/test2/shard3/db
mkdir -p /logs/test2/shard3/log
mkdir -p /data/test2/config/db
mkdir -p /logs/test2/config/log
mkdir -p /logs/test2/mongs/log
mkdir -p /etc/test2

root@evassh-6088960:/etc/test2# cat mongod1.conf mongod2.conf mongod3.conf
# mongod1.conf
dbpath=/data/test2/shard1/db
logpath=/logs/test2/shard1/log/mongodb.log
port=21001
shardsvr=true
fork=true
# mongod2.conf
dbpath=/data/test2/shard2/db
logpath=/logs/test2/shard2/log/mongodb.log
port=21002
shardsvr=true
fork=true
# mongod3.conf
dbpath=/data/test2/shard3/db
logpath=/logs/test2/shard3/log/mongodb.log
port=21003
shardsvr=true
fork=true

# 启动mongo
mongod -f /etc/mongo/mongod1.conf
mongod -f /etc/mongo/mongod2.conf
mongod -f /etc/mongo/mongod3.conf

# 配置config
mongod --dbpath /data/test2/config/db --logpath /logs/test2/config/log/mongodb.log --port 21004 --configsvr --replSet cs --fork
# 链接21004
mongo localhost:21004

use admin
cfg = {
_id:'cs',
configsvr:true,
members:[
{_id:0,host:'localhost:21004'}
]
}
rs.initiate(cfg)

# 配置route
mongos --configdb cs/localhost:21004 --logpath /logs/test2/mongs/log/mongodb.log --port 21005 --fork
# 连接 21005
mongo localhost:21005

sh.addShard('localhost:21001')
sh.addShard('localhost:21002')
sh.addShard('localhost:21003')

第五章 数据备份和恢复

第一关 数据备份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 命令行
mongoimport -d test1 -c person --type json --file /home/example/person.json

mongoimport -d test2 -c student --type csv --headerline --ignoreBlanks --file /home/example/student.csv

# 将所有数据库被分到/opt/mongodb
mongodump -h 127.0.0.1:27017 -o /opt/mongodb
# 将 test1 数据库备份到 /opt/mongodb_1 目录下;
mongodump -h 127.0.0.1:27017 -d test1 -o /opt/mongodb_1
# 将 person 集合备份到 /opt/collection_1 目录下;
mongodump -h 127.0.0.1:27017 -d test1 -c person -o /opt/collection_1
# 将 student 集合压缩备份到 /opt/collection_2 目录下;
mongodump -h 127.0.0.1:27017 -d test2 -c student -o /opt/collection_2 --gzip
# 将 test2 数据库压缩备份到 /opt/mongodb_2 目录下。
mongodump -h 127.0.0.1:27017 -d test2 -o /opt/mongodb_2 --gzip

第二关 数据恢复

1
2
3
4
5
6
7
8
9
10
# 将 /opt/mongodb 目录下的数据恢复到 MongoDB 中;
mongorestore -h 127.0.0.1:27017 /opt/mongodb
# 将 /opt/mongodb_1 目录下的数据恢复到 mytest1 数据库中;
mongorestore -h 127.0.0.1:27017 -d mytest1 --drop /opt/mongodb_1/test1/
# 将 /opt/collection_1 目录下的数据恢复到 mytest2 数据库的 person 集合中;
mongorestore -h 127.0.0.1:27017 -d mytest2 -c person --drop /opt/collection_1/test1/person.bson
# 将 /opt/collection_2 目录下的数据恢复到 mytest3 数据库的 student 集合中,并删除之前备份的表;
mongorestore -h 127.0.0.1:27017 -d mytest3 --drop -c student --gzip /opt/collection_2/test2/student.bson.gz
# 将 /opt/mongodb_2 目录下的数据恢复到 mytest4 的数据库中,并删除之前的备份的数据库。
mongorestore -h 127.0.0.1:27017 -d mytest4 --drop -c student --gzip /opt/mongodb_2/test2/student.bson.gz

第六章

第一关 优化查询原则

CD AD AC ABD BCD ABCD CD AD

第二关 MongoDB 的 Profiling 工具(一)

image.png

第三关 MongoDB 的 Profiling 工具(二)

1
2
3
4
5
use mydb3
db.setProfilingLevel(1,5)

for(var i=0;i<100000;i++)db.items1.insert({_id:i,text:"Hello MongoDB"+i})
for(var i=0;i<100000;i++)db.items2.insert({_id:i,text:"Hello MongoDB"+i})

第七章

第一关 Java 操作 MongoDB 数据库(一)

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
package step1;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.ArrayList;
import java.util.List;
import org.bson.Document;
import com.mongodb.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.FindIterable;
import com.mongodb.Block;
public class Mongoconnect{
public static void main( String args[] ){
Logger log = Logger.getLogger("org.mongodb.driver");
log.setLevel(Level.OFF); //屏蔽带时间的输出
try{
//在下面补充代码,连接到mongodb服务
/********* Begin *********/
MongoClient mongoClient = new MongoClient("localhost",27017); //启动本地服务,端口号为27020
MongoDatabase mongoDatabase = mongoClient.getDatabase("databaseName"); //连接名为databaseName数据库

/********* End *********/
//在下面补充代码,创建集合test1
/********* Begin *********/
mongoDatabase.createCollection("test1"); //创建集合test【如果存在将这一行内容注释】
/********* End *********/
//在下面补充代码,获取集合test1
/********* Begin *********/
MongoCollection<Document> collection = mongoDatabase.getCollection("test1");
/********* End *********/
//在下面补充代码,插入编程要求中的数据到集合test1
/********* Begin *********/
Document document1 = new Document(); //创建一条文档 document1,以下代码为向文档 document1 中追加数据
document1.append("_id", "1");
document1.append("name", "Xiaoming");
document1.append("sex", "man");
document1.append("age", 21);
List<Document> documents = new ArrayList<Document>(); //将以上文档打包存放,为文档插入做准备
documents.add(document1);
collection.insertMany(documents); //插入多条文档到集合中
/********* End *********/
//在Begin和End之间补充代码,请勿修改代码的原本框架
FindIterable<Document> iter = collection.find();
iter.forEach(new Consumer<Document>() {
@Override
public void accept(Document document) {
System.out.println(document.toJson());
}
});
Document doc = collection.find().first();
collection.deleteOne(doc);
}catch (Exception e) {
System.err.println( e.getClass().getName() + ": " + e.getMessage() );
}
}
}}

第二关 Java 操作 MongoDB 数据库(二)

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
package step2;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.bson.Document;
import com.mongodb.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.FindIterable;
import com.mongodb.Block;
import com.mongodb.client.model.Filters;
import com.mongodb.client.MongoCursor;
public class Mongo{
public static void main( String args[] ){
Logger log = Logger.getLogger("org.mongodb.driver");
log.setLevel(Level.OFF); //屏蔽带时间的输出
try{
//仿照第一关,连接数据库mydb2并选择集合test2
/********* Begin *********/
MongoClient mongoClient = new MongoClient("localhost",27017); //启动本地服务,端口号为27017
MongoDatabase mongoDatabase = mongoClient.getDatabase("mydb2"); //连接名为mydb2数据库
MongoCollection<Document> collection = mongoDatabase.getCollection("test2");

/********* End *********/
//在下面补充代码,插入文档到集合test2中
/********* Begin *********/
Document document1 = new Document();
document1.append("_id", "1");
document1.append("name", "Xiaoming");
document1.append("sex", "man");
document1.append("age", 21);

Document document2 = new Document();
document2.append("_id", "2");
document2.append("name", "Xiaohong");
document2.append("sex", "woman");
document2.append("age", 20);

Document document3 = new Document();
document3.append("_id", "3");
document3.append("name", "Xiaoliang");
document3.append("sex", "man");
document3.append("age", 22);

List<Document> documents = new ArrayList<Document>(); //将以上文档打包存放,为文档插入做准备
documents.add(document1);
documents.add(document2);
documents.add(document3);

collection.insertMany(documents); //插入多条文档到集合中
/********* End *********/
//在Begin和End之间补充代码,请勿修改代码的原本框架
FindIterable<Document> iter = collection.find();
System.out.println("文档插入结果如下:");
iter.forEach(new Block<Document>() {
public void apply(Document _doc) {
System.out.println(_doc.toJson());
}
});
//在下面补充代码,更新 Xiaohong 的信息为23岁
/********* Begin *********/
collection.updateMany(Filters.eq("name", "Xiaohong"), new Document("$set",new Document("age",23)));

/********* End *********/
//在Begin和End之间补充代码,请勿修改代码的原本框架
FindIterable<Document> findIterable = collection.find();
MongoCursor<Document> mongoCursor = findIterable.iterator();
System.out.println("更新后文档内容如下:");
while(mongoCursor.hasNext()){
System.out.println(mongoCursor.next());
};
//在下面补充代码,删除Xiaoliang的信息
/********* Begin *********/
collection.deleteOne(Filters.eq("name", "Xiaoliang"));
/********* End *********/
//在Begin和End之间补充代码,请勿修改代码的原本框架
FindIterable<Document> iter1 = collection.find();
System.out.println("删除信息后的文档内容为:");

iter1.forEach(new Consumer<Document>() {
@Override
public void accept(Document document) {
System.out.println(document.toJson());
}
});

collection.drop();
}catch (Exception e) {
System.err.println( e.getClass().getName() + ": " + e.getMessage() );
}
}
}

本文转载自: 掘金

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

《蹲坑也能进大厂》多线程系列 - 线程同步神器七星刀之Sem

发表于 2021-07-19

前言

多线程系列我们前面已经更新过多个章节,强烈建议小伙伴按照顺序学习:

《蹲坑也能进大厂》多线程系列文章目录

Semaphore称为信号量,是JUC中常见的一种工具类,该类使用起来比较简单,唯一的难点就是很多小伙伴们,对于信号量是什么很难理解,今天花Gie就来简单扼要的分析一波。

一、信号量是什么

  • 生活场景举例

我国有很多污染比较严重的工厂,但是为了经济发展,又不能对其全部关停,因此国家就有一个折中的办法,每个工厂需要向环境保护行政主管部门申请排污许可证。只要拥有许可证的工厂才允许合法运作。但是每年的排污许可证又是有数量限制,不能允许过多的工厂申请,否则就无法起到环境污染程度的控制的。

  • Java中信号量

在Java中,Semaphore就是上面的排污许可证,也就是信号量。那我们有哪些场景会用到信号量呢,比如我们调用某个方法,而该方法内部是下载文件或大数据量处理操作,所以非常耗时,这个时候如果我们不对该方法加以限制,当一次性过多线程调用时,可能会拖垮整个服务,Semaphore在这里就起到限制访问线程数量的作用。

image.png

二、Semaphore核心方法

Semaphore和CountDownLatch一样,内部维护一个核心属性sync,通过AQS的共享锁机制实现,这个后续会AQS会详细介绍

1
arduino复制代码private final Sync sync;

看一下Semaphore的几个核心方法:

  • Semaphore(int premits,boolean fair): 构造器方法。permits为信号量初始化数量,第二个参数fair可以设置是否需要公平策略,如果传入true,那么Semaphore会把等待的线程放入FIFO队列中,以便许可证被释放后,可以分配给等待时间最长的线程;
  • acquire(): 试图获取许可证,如果当前没有可用的,就会进入阻塞等待状态;
  • tryAcquire(): 试图获取许可证,如果是否能够获取,都不会进入阻塞。
  • tryAcquire(long timeout, TimeUnit unit): 和tryAcquire一样,只是多了一个超时时间,等待指定时间还获取不到许可证,就会停止等待;
  • availablePermits: 获取可用许可证数量;
  • release(): 释放一个许可证;
  • release(int permits): 释放指定数量的许可证。

三、代码演示

现在创建100个线程,每个线程每次只获取(acquire(1))一个信号量,并将semaphore数量初始化为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
csharp复制代码public class SemaphoreDemo {
   static Semaphore semaphore = new Semaphore(3,true);
   public static void main(String[] args) {
       ExecutorService service = Executors.newFixedThreadPool(5);
       for (int i = 0; i < 100; i++) {
           service.submit(new Runnable() {
               @Override
               public void run() {
                   try {
                       semaphore.acquire(1);
                  } catch (InterruptedException e) {
                       e.printStackTrace();
                  }
                   System.out.println(Thread.currentThread().getName()+"成功获取许可证");
                   try {
                       Thread.sleep(1000);
                  } catch (InterruptedException e) {
                       e.printStackTrace();
                  }
                   System.out.println(Thread.currentThread().getName()+"释放许可证");
                   semaphore.release();
              }
          });
      }
       service.shutdown();
       semaphore.tryAcquire();
  }
}

打印结果如下:

image.png

可以清楚的看到,每次会有三个线程获取到信号量,只有信号量被释放后,其他线程才能继续获取。

代码过程图示如下:

  • 初始化semaphore许可证数量为3;
  • 线程1、线程2、线程3可以正常获取许可证,并方位服务;
  • 线程4访问时由于许可证数量不足,进入阻塞;
  • 当线程1释放许可证后,线程4使用acqire获取,正常访问服务。

image.png

上面我们演示了:使用semaphore.acquire()来获取信号量,这时每个线程只会拿到一个信号量,但如果我们将代码改成acquire(2)呢,这样会出现什么情况,这个时候会有几个线程能够同时执行呢,接下来继续代码演示一下。

1
2
3
scss复制代码//只需要修改两处代码
semaphore.acquire(2);
semaphore.release(1);

结果如下:

image.png

其实这个是很容易理解的,因为一个线程一次获取两个许可证,但是只释放一个,所以许可证两轮之后不足以被其他线程再次获取,其他线程就会被阻塞。

四、注意事项

  • 获取和释放的许可证数量必须一致,否则随着时间的推移,最后许可证数量不够用,会导致线程卡死。
  • Semaphore设置是否公平性时,一般设置为true比较合理,因为Semaphore使用场景就是用在耗时较长的操作,如果被反复插队,线程就会持续陷入等待。
  • 获取和释放的许可证不要求为同一个线程,只要满足我们业务需要,可以由A线程获取许可证,让B线程来释放。

总结

以上就是关于信号量的全部内容,总体看来,用法比较简单,再结合实际场景中许可证的栗子,我们掌握Semaphore会容易很多,今天又是收获满满的一天,下一章说些什么呢,花哥会和大家分享另外一个有趣的知识点-Condition,我们下期见咯。

点关注,防走丢

以上就是本期全部内容,如有纰漏之处,请留言指教,非常感谢。我是花Gie ,有问题大家随时留言讨论 ,我们下期见🦮。

文章持续更新,可以微信搜一搜 【花哥编程】 第一时间阅读,并且可以获取面试资料学习视频等,有兴趣的小伙伴欢迎关注,一起学习,一起哈🐮🥃。

原创不易,你怎忍心白嫖,如果你觉得这篇文章对你有点用的话,感谢老铁为本文点个赞、评论或转发一下,因为这将是我输出更多优质文章的动力,感谢!

本文转载自: 掘金

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

一文搞懂什么是SQL注入---SQL注入详解 一:什么是sq

发表于 2021-07-19

一:什么是sql注入

SQL注入是比较常见的网络攻击方式之一,它不是利用操作系统的BUG来实现攻击,而是针对程序员编写时的疏忽,通过SQL语句,实现无账号登录,甚至篡改数据库。

二:SQL注入攻击的总体思路

  • 寻找到SQL注入的位置
  • 判断服务器类型和后台数据库类型
  • 针对不同的服务器和数据库特点进行SQL注入攻击

三:SQL注入攻击实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String复制代码' "+userName+" ' and password=' "+password+" '";

--当输入了上面的用户名和密码,上面的SQL语句变成:
SELECT * FROM user_table WHERE username=
'’or 1 = 1 -- and password='’

"""
--分析SQL语句:
--条件后面username=”or 1=1 用户名等于 ” 或1=1 那么这个条件一定会成功;

--然后后面加两个-,这意味着注释,它将后面的语句注释,让他们不起作用,这样语句永远都--能正确执行,用户轻易骗过系统,获取合法身份。
--这还是比较温柔的,如果是执行
SELECT * FROM user_table WHERE
username='' ;DROP DATABASE (DB Name) --' and password=''
--其后果可想而知…
"""

四:如何防御SQL注入

注意:但凡有SQL注入漏洞的程序,都是因为程序要接受来自客户端用户输入的变量或URL传递的参数,并且这个变量或参数是组成SQL语句的一部分,对于用户输入的内容或传递的参数,我们应该要时刻保持警惕,这是安全领域里的「外部数据不可信任」的原则,纵观Web安全领域的各种攻击方式,大多数都是因为开发者违反了这个原则而导致的,所以自然能想到的,就是从变量的检测、过滤、验证下手,确保变量是开发者所预想的。

1、检查变量数据类型和格式

如果你的SQL语句是类似where id={$id}这种形式,数据库里所有的id都是数字,那么就应该在SQL被执行前,检查确保变量id是int类型;如果是接受邮箱,那就应该检查并严格确保变量一定是邮箱的格式,其他的类型比如日期、时间等也是一个道理。总结起来:只要是有固定格式的变量,在SQL语句执行前,应该严格按照固定格式去检查,确保变量是我们预想的格式,这样很大程度上可以避免SQL注入攻击。
  比如,我们前面接受username参数例子中,我们的产品设计应该是在用户注册的一开始,就有一个用户名的规则,比如5-20个字符,只能由大小写字母、数字以及一些安全的符号组成,不包含特殊字符。此时我们应该有一个check_username的函数来进行统一的检查。不过,仍然有很多例外情况并不能应用到这一准则,比如文章发布系统,评论系统等必须要允许用户提交任意字符串的场景,这就需要采用过滤等其他方案了。

2、过滤特殊符号

对于无法确定固定格式的变量,一定要进行特殊符号过滤或转义处理。

3、绑定变量,使用预编译语句

MySQL的mysqli驱动提供了预编译语句的支持,不同的程序语言,都分别有使用预编译语句的方法

实际上,绑定变量使用预编译语句是预防SQL注入的最佳方式,使用预编译的SQL语句语义不会发生改变,在SQL语句中,变量用问号?表示,黑客即使本事再大,也无法改变SQL语句的结构

五:什么是sql预编译

1.1:预编译语句是什么

通常我们的一条sql在db接收到最终执行完毕返回可以分为下面三个过程:

词法和语义解析
优化sql语句,制定执行计划
执行并返回结果
我们把这种普通语句称作Immediate Statements。

但是很多情况,我们的一条sql语句可能会反复执行,或者每次执行的时候只有个别的值不同(比如query的where子句值不同,update的set子句值不同,insert的values值不同)。
  如果每次都需要经过上面的词法语义解析、语句优化、制定执行计划等,则效率就明显不行了。

所谓预编译语句就是将这类语句中的值用占位符替代,可以视为将sql语句模板化或者说参数化,一般称这类语句叫Prepared Statements或者Parameterized Statements
  预编译语句的优势在于归纳为:一次编译、多次运行,省去了解析优化等过程;此外预编译语句能防止sql注入。
  当然就优化来说,很多时候最优的执行计划不是光靠知道sql语句的模板就能决定了,往往就是需要通过具体值来预估出成本代价。

1.2:MySQL的预编译功能

注意MySQL的老版本(4.1之前)是不支持服务端预编译的,但基于目前业界生产环境普遍情况,基本可以认为MySQL支持服务端预编译。

下面我们来看一下MySQL中预编译语句的使用。

(1)建表

首先我们有一张测试表t,结构如下所示:

1
2
3
4
5
6
7
8
sql复制代码mysql> show create table t\G
*************************** 1. row ***************************
Table: t
Create Table: CREATE TABLE `t` (
`a` int(11) DEFAULT NULL,
`b` varchar(20) DEFAULT NULL,
UNIQUE KEY `ab` (`a`,`b`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

(2)编译

我们接下来通过 PREPARE stmt_name FROM preparable_stm的语法来预编译一条sql语句

1
2
3
sql复制代码mysql> prepare ins from 'insert into t select ?,?';
Query OK, 0 rows affected (0.00 sec)
Statement prepared

(3)执行

我们通过EXECUTE stmt_name [USING @var_name [, @var_name] …]的语法来执行预编译语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码mysql> set @a=999,@b='hello';
Query OK, 0 rows affected (0.00 sec)

mysql> execute ins using @a,@b;
Query OK, 1 row affected (0.01 sec)
Records: 1 Duplicates: 0 Warnings: 0

mysql> select * from t;
+------+-------+
| a | b |
+------+-------+
| 999 | hello |
+------+-------+
1 row in set (0.00 sec)

可以看到,数据已经被成功插入表中。

MySQL中的预编译语句作用域是session级,但我们可以通过max_prepared_stmt_count变量来控制全局最大的存储的预编译语句。

1
2
3
4
5
vbnet复制代码mysql> set @@global.max_prepared_stmt_count=1;
Query OK, 0 rows affected (0.00 sec)

mysql> prepare sel from 'select * from t';
ERROR 1461 (42000): Can't create more than max_prepared_stmt_count statements (current value: 1)

当预编译条数已经达到阈值时可以看到MySQL会报如上所示的错误。

(4)释放

如果我们想要释放一条预编译语句,则可以使用{DEALLOCATE | DROP} PREPARE stmt_name的语法进行操作:

1
2
sql复制代码mysql> deallocate prepare ins;
Query OK, 0 rows affected (0.00 sec)

六:为什么PrepareStatement可以防止sql注入

原理是采用了预编译的方法,先将SQL语句中可被客户端控制的参数集进行编译,生成对应的临时变量集,再使用对应的设置方法,为临时变量集里面的元素进行赋值,赋值函数setString(),会对传入的参数进行强制类型检查和安全检查,所以就避免了SQL注入的产生。下面具体分析

(1)为什么Statement会被sql注入

因为Statement之所以会被sql注入是因为SQL语句结构发生了变化。比如:

1
2
arduino复制代码"select*from tablename where username='"+uesrname+  
"'and password='"+password+"'"

在用户输入’or true or’之后sql语句结构改变。

1
python复制代码select*from tablename where username=''or true or'' and password=''

这样本来是判断用户名和密码都匹配时才会计数,但是经过改变后变成了或的逻辑关系,不管用户名和密码是否匹配该式的返回值永远为true;

(2)为什么Preparement可以防止SQL注入

因为Preparement样式为

1
csharp复制代码select*from tablename where username=? and password=?

该SQL语句会在得到用户的输入之前先用数据库进行预编译,这样的话不管用户输入什么用户名和密码的判断始终都是并的逻辑关系,防止了SQL注入.

简单总结,参数化能防注入的原因在于,语句是语句,参数是参数,参数的值并不是语句的一部分,数据库只按语句的语义跑,至于跑的时候是带一个普通背包还是一个怪物,不会影响行进路线,无非跑的快点与慢点的区别。

本文转载自: 掘金

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

1…600601602…956

开发者博客

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