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

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


  • 首页

  • 归档

  • 搜索

Spring Security 如何将用户数据存入数据库?(

发表于 2021-11-23

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

写在前面

Spring Security 专题好久没更新了。Spring Security 介绍到现在,我们还没连上数据库呢。我们今天接着学习。

这里多唠叨一句,欢迎大家查看我的专栏,目前正在进行的Security专栏和队列并发专栏,设计模式专题(已完结)

1.UserDetailService

Spring Security 支持多种不同的数据源,这些不同的数据源最终都将被封装成 UserDetailsService 的实例

我们来看下 UserDetailsService 都有哪些实现类:

image.png

可以看到,在几个能直接使用的实现类中,除了 InMemoryUserDetailsManager 之外,还有一个 JdbcUserDetailsManager,使用 JdbcUserDetailsManager 可以让我们通过 JDBC 的方式将数据库和 Spring Security 连接起来。

JdbcUserDetailsManager

JdbcUserDetailsManager 自己提供了一个数据库模型,这个数据库模型保存在如下位置:

org/springframework/security/core/userdetails/jdbc/users.ddl

大家可自行去查看

这里存储的脚本内容如下:

1
2
3
4
5
java复制代码create table users(username varchar(50) not null primary key,password varchar(500) not null,enabled boolean not null);

create table authorities (username varchar(50) not null,authority varchar(50) not null,constraint fk_authorities_users foreign key(username) references users(username));

create unique index ix_auth_username on authorities (username,authority);

执行完 SQL 脚本后,我们可以看到一共创建了两张表:users 和 authorities。

  • users 表中保存用户的基本信息,包括用户名、用户密码以及账户是否可用。
  • authorities 中保存了用户的角色。
  • authorities 和 users 通过 username 关联起来。

配置完成后,接下来,我们将上篇文章中通过 InMemoryUserDetailsManager 提供的用户数据用 JdbcUserDetailsManager 代替掉,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Autowired
DataSource dataSource;
@Override
@Bean
protected UserDetailsService userDetailsService() {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
manager.setDataSource(dataSource);
if (!manager.userExists("shushi")) {
manager.createUser(User.withUsername("shushi").password("123").roles("admin").build());
}
if (!manager.userExists("一点东西")) {
manager.createUser(User.withUsername("一点东西").password("123").roles("user").build());
}
return manager;
}

这段配置的含义如下:

  • 首先构建一个 JdbcUserDetailsManager 实例。
  • 给 JdbcUserDetailsManager 实例添加一个 DataSource 对象。
  • 调用 userExists 方法判断用户是否存在,如果不存在,就创建一个新的用户出来(因为每次项目启动时这段代码都会执行,所以加一个判断,避免重复创建用户)。
  • 用户的创建方法和我们之前 InMemoryUserDetailsManager 中的创建方法基本一致。

下面我们进入数据库的配置

数据库支持

通过前面的代码,大家看到这里需要数据库支持,所以我们在项目中添加如下两个依赖:

1
2
3
4
5
6
7
8
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

配置文件中配置下

1
2
3
java复制代码spring.datasource.username=root\
spring.datasource.password=123\
spring.datasource.url=jdbc:mysql:///security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

配置完成后,就可以启动项目。

项目启动成功后,我们就可以看到数据库中自动添加了两个用户进来,并且用户都配置了角色。如下图:

users表:

image.png

authorities表:

image.png

进行测试

我们首先以 一点东西的身份进行登录:

登录成功后,分别访问 /hello,/admin/hello 以及 /user/hello 三个接口,其中:

  • /hello 因为登录后就可以访问,这个接口访问成功。
  • /admin/hello 需要 admin 身份,所以访问失败。
  • /user/hello 需要 user 身份,所以访问成功。
    具体测试效果 我就不截图了。 目的是让大家亲自试下。

在测试的过程中,如果在数据库中将用户的 enabled 属性设置为 false,表示禁用该账户,此时再使用该账户登录就会登录失败。

按照相同的方式,大家也可以测试 javaboy 用户。

好了,今天关于Spring Security将用户数据存入数据库的学习就到这里 我们下期再见 加油!!!

弦外之音

感谢你的阅读,如果你感觉学到了东西,您可以点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作

本文转载自: 掘金

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

🏆【Alibaba中间件技术系列】「RocketMQ技术专题

发表于 2021-11-23

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

前提介绍

在RocketMQ中一般有两种获取消息的方式,一个是拉(pull,消费者主动去broker拉取),一个是推(push,主动推送给消费者),在上一章节中已经介绍到了相关的Push操作,接下来的章节会介绍Pull操作方式的消费机制体系。

DefaultMQPullConsumer

DefaultMQPullConsumer与DefaultMQPushConsumer相比最大的区别是,消费哪些队列的消息,从哪个位移开始消费,以及何时提交消费位移都是由程序自己的控制的。下面来介绍一下DefaultMQPullConsumer的内部原理。

总体流程执行

DefaultMQPullConsumer使用例子

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
java复制代码public class MQPullConsumer {
private static final Map<MessageQueue,Long> OFFSE_TABLE = new HashMap<MessageQueue,Long>();
public static void main(String[] args) throws MQClientException {
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("groupName");
consumer.setNamesrvAddr("name-serverl-ip:9876;name-server2-ip:9876");
consumer.start();
// 从指定topic中拉取所有消息队列
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("order-topic");
for(MessageQueue mq:mqs){
try {
// 获取消息的offset,指定从store中获取
long offset = consumer.fetchConsumeOffset(mq,true);
while(true){
PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
putMessageQueueOffset(mq,pullResult.getNextBeginOffset());
switch(pullResult.getPullStatus()){
case FOUND:
List<MessageExt> messageExtList = pullResult.getMsgFoundList();
for (MessageExt m : messageExtList) {
System.out.println(new String(m.getBody()));
}
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break;
case OFFSET_ILLEGAL:
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
consumer.shutdown();
}
// 保存上次消费的消息下标
private static void putMessageQueueOffset(MessageQueue mq,
long nextBeginOffset) {
OFFSE_TABLE.put(mq, nextBeginOffset);
}
// 获取上次消费的消息的下标
private static Long getMessageQueueOffset(MessageQueue mq) {
Long offset = OFFSE_TABLE.get(mq);
if(offset != null){
return offset;
}
return 0l;
}
}
  • 消费者启动:consumer.start();
  • 获取主题下所有的消息队列:这里是根据topic从nameserver获取的这里我们可以修改为从其他位置获取队列信息
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("topicTest");
//遍历队列
for(MessageQueue mq:mqs){
try {
//获取当前队列的消费位移,第二个参数表示位移是从本地内存获取,还是从broker获取,true表示从broker获取
long offset = consumer.fetchConsumeOffset(mq,true);
while(true){
//第二个参数表示可以消费哪些tag的消息
//第三个参数表示从哪个位移开始消费消息
//第四个参数表示一次最大拉多少个消息
PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
}

DefaultMQPullConsumer的总体流程

启动DefaultMQPullConsumer是通过调用start()方法完成的

DefaultMQPullConsumer拉取源码分析

分析下DefaultMQPullConsumer拉取消息的流程

1
java复制代码consumer.fetchSubscribeMessageQueues("order-topic")

从指定topic中拉取所有消息队列

1
java复制代码Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("order-topic");

核心源码分析

fetchSubscribeMessageQueues()
  • 通过调用fetchSubscribeMessageQueues()方法可以获取指定topic(GET_ROUTEINTO_BY_TOPIC)的读队列信息。它通过向nameserver发送GetRouteInfoRequest请求,请求内容为GET_ROUTEINTO_BY_TOPIC,nameserver将主题下的读队列个数发送给消费者,然后消费者使用如下代码创建出与读队列个数相同的MessageQueue对象。
  • 每个MessageQueue对象里面记录了topic、broker名和读队列号。最后fetchSubscribeMessageQueues()将MessageQueue对象集合返回给调用者。
  • 向NameServer发送请求获取topic参数对应的Broker信息和topic配置信息,即TopicRouteData对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码 public Set<MessageQueue> fetchSubscribeMessageQueues(String topic) throws MQClientException {
try {
TopicRouteData topicRouteData = this.mQClientFactory.getMQClientAPIImpl().getTopicRouteInfoFromNameServer(topic, timeoutMillis);
if (topicRouteData != null) {
// 2、遍历topicRouteData
Set<MessageQueue> mqList = MQClientInstance.topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
if (!mqList.isEmpty()) {
return mqList;
} else {
throw new MQClientException("Can not find Message Queue for this topic, " + topic + " Namesrv return empty", null);
}
}
} catch (Exception e) {
throw new MQClientException(
"Can not find Message Queue for this topic, " + topic + FAQUrl.suggestTodo(FAQUrl.MQLIST_NOT_EXIST),
e);
}
throw new MQClientException("Unknow why, Can not find Message Queue for this topic, " + topic, null);
}

遍历过程TopicRouteData

遍历TopicRouteData对象的QueueData列表中每个QueueData对象,首先判断该QueueData对象是否具有读权限,
若有则根据该QueueData对象的readQueueNums值,创建readQueueNums个MessageQueue对象,并构成MessageQueue集合;
最后返回给MessageQueue集合

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public static Set<MessageQueue> topicRouteData2TopicSubscribeInfo(final String topic, final TopicRouteData route) {
Set<MessageQueue> mqList = new HashSet<MessageQueue>();
List<QueueData> qds = route.getQueueDatas();
for (QueueData qd : qds) {
if (PermName.isReadable(qd.getPerm())) {
for (int i = 0; i < qd.getReadQueueNums(); i++) {
MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i);
mqList.add(mq);
}
}
}
return mqList;
}
consumer.fetchConsumeOffset

通过该方法获取该MessageQueue队列下面从offset位置开始的消息内容,其中maxNums=32即表示获取的最大消息个数,offset为该MessageQueue对象的开始消费位置。

1
java复制代码DefaultMQPullConsumer.fetchConsumeOffset(MessageQueue mq, boolean fromStore)

fetchConsumeOffset()有两个入参,第一个参数表示队列,第二个参数表示是否从broker获取该队列的消费位移,true表示从broker获取,false表示从本地记录获取,如果本地获取不到再从broker获取。
这里说的从本地获取是指从RemoteBrokerOffsetStore.offsetTable属性中获取,该属性记录了每个队列的消费位移。当从broker获取位移后会更新offsetTable。

pullBlockIfNotFound拉取信息

rocketmq提供了多个拉取方法,可以使用pullBlockIfNotFound()方法也可以使用pull()方法。两者的区别是如果队列中没有消息,两个方法的超时时间是不同的,pullBlockIfNotFound会等待30s返回一个空结果,pull是等待10s返回空结果。

不过pull方法的入参可以调整超时时间,而pullBlockIfNotFound则需要修改DefaultMQPullConsumer.consumerPullTimeoutMillis参数。不过两个方法调用的底层逻辑都是一样的,都是调用DefaultMQPullConsumerImpl.pullSyncImpl()方法获取消息。下面分析一下pullSyncImpl()方法。

1
2
3
4
java复制代码public PullResult pullBlockIfNotFound(MessageQueue mq, String subExpression, long offset, int maxNums)
throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
return this.pullSyncImpl(mq, subExpression, offset, maxNums, true, this.getDefaultMQPullConsumer().getConsumerPullTimeoutMillis());
}

获取该MessageQueue队列的消费进度来设定参数offset值该方法最终调用pullSyncImpl,可以获取相关的结果数据。

  • 参数1:消息队列(通过调用消费者的fetchSubscibeMessageQueue(topic)可以得到相应topic的所需要消息队列) ;
  • 参数2:需要过滤用的表达式 ;
  • 参数3:偏移量即消费队列的进度 ;
  • 参数4:一次取消息的最大值 ;
1
java复制代码DefaultMQPullConsumerImpl.pullSyncImpl(MessageQueue mq, String subExpression, long offset, int maxNums, boolean block)
DefaultMQPullConsumerImpl.pullSyncImpl的实现过程
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
java复制代码      private PullResult pullSyncImpl(MessageQueue mq, SubscriptionData subscriptionData, long offset, int maxNums, boolean block,
long timeout)
throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
this.isRunning();
//检查入参是否合法
if (null == mq) {
throw new MQClientException("mq is null", null);
}

if (offset < 0) {
throw new MQClientException("offset < 0", null);
}

if (maxNums <= 0) {
throw new MQClientException("maxNums <= 0", null);
}
//更新再平衡服务的数据,因为再平衡服务不起作用,所以更新数据没有效果
this.subscriptionAutomatically(mq.getTopic());

int sysFlag = PullSysFlag.buildSysFlag(false, block, true, false);
//计算超时时间,如果调用的是pullBlockIfNotFound方法,block参数就是true,否则就是false
long timeoutMillis = block ? this.defaultMQPullConsumer.getConsumerTimeoutMillisWhenSuspend() : timeout;

boolean isTagType = ExpressionType.isTagType(subscriptionData.getExpressionType());
//调用PullAPIWrapper从broker拉取消息,
//pullKernelImpl方法里面构建PullMessageRequest请求对象
PullResult pullResult = this.pullAPIWrapper.pullKernelImpl(
mq,//队列
subscriptionData.getSubString(),//消息的过滤规则
subscriptionData.getExpressionType(),
isTagType ? 0L : subscriptionData.getSubVersion(),
offset,//拉取消息的位移
maxNums,//建议broker一次性返回最大消息个数,默认是32个
sysFlag,
0,//设置的提交位移,可以看到永远都是0,所以broker无法记录有效位移,需要程序自己记录控制提交位移
this.defaultMQPullConsumer.getBrokerSuspendMaxTimeMillis(),
timeoutMillis,//超时时间
CommunicationMode.SYNC,
null//回调逻辑为null
);
this.pullAPIWrapper.processPullResult(mq, pullResult, subscriptionData);
//If namespace is not null , reset Topic without namespace.
this.resetTopic(pullResult.getMsgFoundList());
if (!this.consumeMessageHookList.isEmpty()) {
ConsumeMessageContext consumeMessageContext = null;
consumeMessageContext = new ConsumeMessageContext();
consumeMessageContext.setNamespace(defaultMQPullConsumer.getNamespace());
consumeMessageContext.setConsumerGroup(this.groupName());
consumeMessageContext.setMq(mq);
consumeMessageContext.setMsgList(pullResult.getMsgFoundList());
consumeMessageContext.setSuccess(false);
this.executeHookBefore(consumeMessageContext);
consumeMessageContext.setStatus(ConsumeConcurrentlyStatus.CONSUME_SUCCESS.toString());
consumeMessageContext.setSuccess(true);
this.executeHookAfter(consumeMessageContext);
}
return pullResult;
}

检查MessageQueue对象的topic是否在RebalanceImpl.subscriptionInner:ConcurrentHashMap<String,SubscriptionData>变量中,若不在则以consumerGroup、topic、subExpression为参数调用FilterAPI.buildSubscriptionData(String consumerGroup, String topic, String subExpression)方法构造SubscriptionData对象保存到RebalanceImpl.subscriptionInner变量中,其中 subExpression=”*“
this.subscriptionAutomatically(mq.getTopic());
// 构建标志位,逻辑或运算|=
int sysFlag = PullSysFlag.buildSysFlag(false, block, true, false);

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
kotlin复制代码    SubscriptionData subscriptionData;
try {
//以请求参数subExpression以及consumerGroup、topic为参数调用FilterAPI.buildSubscriptionData(String consumerGroup,Stringtopic, String subExpression)方法构造SubscriptionData对象并返回
subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPullConsumer.getConsumerGroup(),
mq.getTopic(), subExpression);
} catch (Exception e) {
throw new MQClientException("parse subscription error", e);
}

long timeoutMillis = block ? this.defaultMQPullConsumer.getConsumerTimeoutMillisWhenSuspend() : timeout;
// 从broker中拉取消息
PullResult pullResult = this.pullAPIWrapper.pullKernelImpl(
mq,
subscriptionData.getSubString(),
0L,
offset,
maxNums,
sysFlag,
0,
this.defaultMQPullConsumer.getBrokerSuspendMaxTimeMillis(),
timeoutMillis,
CommunicationMode.SYNC,
null
);
// 对拉取到的消息进行解码,过滤并执行回调,并把解析的message列表放到MsgFoundList中
this.pullAPIWrapper.processPullResult(mq, pullResult, subscriptionData);
if (!this.consumeMessageHookList.isEmpty()) {
ConsumeMessageContext consumeMessageContext = null;
consumeMessageContext = new ConsumeMessageContext();
consumeMessageContext.setConsumerGroup(this.groupName());
consumeMessageContext.setMq(mq);
consumeMessageContext.setMsgList(pullResult.getMsgFoundList());
consumeMessageContext.setSuccess(false);
this.executeHookBefore(consumeMessageContext);
consumeMessageContext.setStatus(ConsumeConcurrentlyStatus.CONSUME_SUCCESS.toString());
consumeMessageContext.setSuccess(true);
this.executeHookAfter(consumeMessageContext);
}
return pullResult;
}

Push和Pull的操作对比

  • push-优点:及时性、服务端统一处理实现方便
  • push-缺点:容易造成堆积、负载性能不可控
  • pull-优点:获得消息状态方便、负载均衡性能可控
  • pull-缺点:及时性差

使用DefaultMQPullConsumer拉取消息,发送到broker的提交位移永远都是0,所以broker无法记录有效位移,需要程序自己记录和控制提交位移。

资料参考

  • blog.csdn.net/weixin_3830…
  • blog.csdn.net/weixin_3830…

本文转载自: 掘金

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

2018年世界杯德国竟然输给韩国?终于找到原因了!

发表于 2021-11-23

程序员宝藏库:github.com/Jackpopc/CS…

大家好,我是Jackpop。

今天来跟大家聊一下足球。

首先表明,我并不是一个足球爱好者。

我很少关注世界杯、欧冠这些名气较大的足球比赛,更不会去留意国足相关的内容。

虽然对于足球比赛竞技本身并不感兴趣,但是作为一个数据科学领域的开发人员,对比赛背后的数据还是充满着浓厚的兴趣。

数据科学是一种从数据中挖掘和捕捉有价值信息的方法,数据科学正在影响着许多领域,这也包括足球。

足球包含大量的数据,从个人到团队方面都有涉及。有了这些数据,我们就能以一种更有意义的方式了解比赛。

另外,对于球队来说,数据可以创造出指导决策的信息。

因此,团队可以找到赢得比赛的策略。

在这篇文章中,我将带你了解如何使用Python分析足球赛事数据。

在这里,我们将分析2018年国际足联世界杯德国和韩国的比赛。

不用多说,让我们开始吧!

数据获取

对于数据,我们将使用StatsBomb的数据。

StatsBomb是一家专门在足球领域工作的分析公司,他们提供了大量的足球数据,尤其是事件数据。

对于那些想学习足球分析的人来说,得益于StatsBomb已经公布了公开数据,能够节省很多获取数据的时间。

这些数据由已经结束的足球联赛的比赛组成,数据链接:github.com/statsbomb/o…

注意:在获取数据时,请耐心等待,因为数据量真的很大。

数据探索

在你下载数据后,下一步是探索它。

数据的文件夹结构如下所示:

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
sql复制代码|   LICENSE.pdf
| README.md
|
+---data
| | competitions.json
| |
| +---events
| | 15946.json
| | 15956.json
| | 15973.json
| | 15978.json
| | 15986.json
| |
| +---lineups
| | 15946.json
| | 15956.json
| | 15973.json
| | 15978.json
| | 15986.json
| |
| \---matches
| +---11
| | 1.json
| | 2.json
| |
| +---16
| | 1.json
| | 2.json
| |
|
+---doc
| Open Data Competitions v2.0.0.pdf
| Open Data Events v4.0.0.pdf
| Open Data Lineups v2.0.0.pdf
| Open Data Matches v3.0.0.pdf
| StatsBomb Open Data Specification v1.1.pdf
|
\---img
statsbomb-logo.jpg

还有一些文件夹,如事件(events)、阵容(lineups)和比赛(matches):

  • 事件文件夹包含以JSON格式回顾比赛的文件
  • 阵容文件夹包含每场比赛中各队的阵容
  • 比赛文件夹包含每场比赛的比赛。它也被分为几个比赛的不同季节

那么,在数据里面有很多文件的情况下,我们怎样才能检索到一个特定的比赛呢?正如我之前提到的,我们将分析德国和韩国的世界杯比赛。

在下一步,我将告诉你如何检索数据。

数据检索

事件数据可以通过以下步骤来检索。

首先,我们打开competitions.json文件。这个文件是访问StatsBomb数据第一步要做的事情。

这样做的原因是,我们需要比赛和赛季的ID,以便从中获取比赛的列表。

为了处理JSON文件,pandas库提供了通过使用read_json函数将JSON文件读成DataFrame的函数:

1
2
3
4
python复制代码import pandas as pd

competition = pd.read_json('open-data/data/competitions.json')
competition.head()

现在,你可以看到包含StatsBomb提供的所有比赛信息的行。

img

总之,这些数据中包括的比赛有西甲(西班牙联赛)、欧洲杯、国际足联世界杯(男子和女子)以及欧洲冠军联赛。

现在,我们想取有国际足联世界杯信息的那一行。

让我们用下面这行代码来过滤数据集。

1
2
python复制代码# Get the FIFA World Cup
competition[competition.competition_name == 'FIFA World Cup']

img

从上面可以看出,国际足联世界杯的比赛和赛季的ID分别是43和3。

现在让我们来访问包含ID的文件夹。

对于每个比赛,文件夹都是以比赛ID命名的,而每个文件夹都包含JSON文件。

每个文件都是以赛季ID为名称的附件。

现在让我们通过使用这几行代码来访问该文件:

1
2
3
4
5
6
python复制代码import json

with open('open-data/data/matches/43/3.json') as f:
data = json.load(f)

data

哇,你会发现,这是个很大的数据,而且读起来很混乱。

让我们首先通过使用循环来整理它。

在每一次迭代中,我们都要取得比赛的ID、球队的名字和分数:

1
2
3
4
python复制代码with open('open-data/data/matches/43/3.json') as f:
data = json.load(f)
for i in data:
print('ID:', i['match_id'], i['home_team']['home_team_name'], i['home_score'], '-', i['away_score'], i['away_team']['away_team_name'])

现在,它比以前更整洁了。

让我们来看看德国对韩国的比赛。

最后的比分是2-0,韩国是赢家(你可能不知道,德国和韩国之间的比赛是惊人的)。

这个结果也使得德国人在小组赛阶段就被淘汰出局,这是自1938年以来,德国第一次在第一轮比赛中被淘汰。

回到主题,德国对阵韩国的比赛ID是7567。

通过如下几行代码来访问这个文件:

1
2
3
4
python复制代码with open('open-data/data/events/7567.json') as f:
korger = json.load(f)

korger

这比之前的数据要多得多。

为了便于我们分析,pandas库提供了json_normalize函数,这个函数之所以如此强大,是因为它可以处理嵌套的JSON。

现在我们来写这几行代码:

1
2
3
4
python复制代码# from pandas.io.json import json_normalize

df = pd.json_normalize(korger, sep='_').assign(match_id="7567")
df.head()

img

比以前更有可读性,接下来让我们从数据中创建一些可视化的东西。

数据可视化

我们可以创建的可视化之一是射门图。

在这张图上,我们想看看每支球队有多少次射门。此外,还想知道进球的几率有多大,我们称这种机会为预期进球。

为了创建可视化,我们需要首先获取事件数据。然后,根据事件名称来过滤数据。

在这种情况下,我们需要射门的相关数据:

1
2
python复制代码shots = df[df.type_name == 'Shot'].set_index('id')
shots.head()

img

在我们得到数据后,现在让我们来写这些代码,对数据进行可视化:

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
python复制代码import numpy as np
import matplotlib.pyplot as plt
from FCPython import createPitch

pitch_width = 120
pitch_height = 80

fig, ax = createPitch(pitch_width, pitch_height, 'yards', 'gray')

home_team = 'South Korea'
away_team = 'Germany'

for i, shot in shots.iterrows():
x = shot['location'][0]
y = shot['location'][1]

goal = shot['shot_outcome_name']=='Goal'
team_name = shot['team_name']

circle_size = 2
circle_size = np.sqrt(shot['shot_statsbomb_xg'] * 15)

if team_name == home_team:
if goal:
shot_circle = plt.Circle((x, pitch_height-y), circle_size, color='red')
plt.text((x+1), pitch_height-y+1, shot['player_name'])
else:
shot_circle = plt.Circle((x, pitch_height-y), circle_size, color='red')
shot_circle.set_alpha(.2)
elif team_name == away_team:
if goal:
shot_circle = plt.Circle((pitch_width-x, y), circle_size, color='blue')
plt.text((pitch_width-x+1), y+1, shot['player_name'])
else:
shot_circle = plt.Circle((pitch_width-x, y), circle_size, color='blue')
shot_circle.set_alpha(.2)

ax.add_patch(shot_circle)

plt.text(5, 75, away_team + ' shots')
plt.text(80, 75, home_team + ' shots')

plt.title('Germany vs South Korea at 2018 FIFA World Cup')

fig.set_size_inches(10, 7)
fig.savefig('korger_shots.png', dpi=300)

plt.show()

让我先解释一下代码。

首先,创建一个足球场。然后,还要创建一个点的集合,对应于已经进行的射击。

为了创建这个球场,我们可以使用FCPython中的createPitch函数。

关于FCPython,你可以在这里查看[GitHub资源库](SoccermaticsForPython/FCPython.py at master · Friends-of-Tracking-Data-FoTD/SoccermaticsForPython · GitHub)。

为了生成圆点,我们需要遍历dataframe中的数据行。对于每一次迭代,需要做如下两件事情:

  • 把坐标和预期目标(xG)值一起拿出来。
  • 根据前面的参数生成一个圆。xG值将被用作圆圈大小的值,我们还为不是进球的射门设置透明度。

如果你的代码写得正确,它应该产生一个像下面这样的效果图:

img

现在我们可以从数据中得到启示。

正如我们从上面看到的,我们知道德国有很多的机会,但他们无法从中获得任何进球。

另外,他们有几次射门的xG值很大。xG值越大,进球的机会就越大。但不幸的是,德国人无法将其转换为进球。

在韩国方面,我们可以看到,他们没有很多机会。他们也不像德国人那样每次射门都有巨大的预期进球。

但是,在比赛结束时,德国队犯了一些错误,导致了一个尴尬的结果。最后,孙兴民和金英权成了韩国队的英雄。

结语

这是你可以创建的可视化之一,我们可以做的数据可视化有很多。

除此之外,我们可以做一个传球热图,或者进球过程中的控球链,或者每个球员的传球图。

正如我之前所说,足球赛事数据有很多值得挖掘的信息。

因此,这有助于团队和外面的足球爱好者更加了解这个比赛。

现在你已经学会了如何用Python分析StatsBomb的足球事件数据。

我希望它能激励你开始使用Python分析体育数据,特别是足球。


大家好,我是Jackpop!我花费了半个月的时间把这几年来收集的各种技术干货整理到一起,其中内容包括但不限于Python、机器学习、深度学习、计算机视觉、推荐系统、Linux、工程化、Java,内容多达5T+,获取方式:pan.baidu.com/s/1eks7CUyj…

本文转载自: 掘金

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

备忘录模式

发表于 2021-11-23

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

概述

备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原,很多软件都提供了撤销(Undo)操作,如 Word、记事本、Photoshop、IDEA等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 浏览器 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。

定义:

又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。

结构

备忘录模式的主要角色如下:

  • 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
  • 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
  • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。

备忘录有两个等效的接口:

  • 窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。
  • 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。

案例实现

【例】游戏挑战BOSS

游戏中的某个场景,一游戏角色有生命力、攻击力、防御力等数据,在打Boss前和后一定会不一样的,我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗之前的状态。

要实现上述案例,有两种方式:

  • “白箱”备忘录模式
  • “黑箱”备忘录模式

“白箱”备忘录模式

备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开。类图如下:

代码如下:

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
132
133
134
135
136
137
java复制代码//游戏角色类
public class GameRole {
private int vit; //生命力
private int atk; //攻击力
private int def; //防御力

//初始化状态
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}

//战斗
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}

//保存角色状态
public RoleStateMemento saveState() {
return new RoleStateMemento(vit, atk, def);
}

//回复角色状态
public void recoverState(RoleStateMemento roleStateMemento) {
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}

public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + def);
}

public int getVit() {
return vit;
}

public void setVit(int vit) {
this.vit = vit;
}

public int getAtk() {
return atk;
}

public void setAtk(int atk) {
this.atk = atk;
}

public int getDef() {
return def;
}

public void setDef(int def) {
this.def = def;
}
}

//游戏状态存储类(备忘录类)
public class RoleStateMemento {
private int vit;
private int atk;
private int def;

public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}

public int getVit() {
return vit;
}

public void setVit(int vit) {
this.vit = vit;
}

public int getAtk() {
return atk;
}

public void setAtk(int atk) {
this.atk = atk;
}

public int getDef() {
return def;
}

public void setDef(int def) {
this.def = def;
}
}

//角色状态管理者类
public class RoleStateCaretaker {
private RoleStateMemento roleStateMemento;

public RoleStateMemento getRoleStateMemento() {
return roleStateMemento;
}

public void setRoleStateMemento(RoleStateMemento roleStateMemento) {
this.roleStateMemento = roleStateMemento;
}
}

//测试类
public class Client {
public static void main(String[] args) {
System.out.println("------------大战Boss前------------");
//大战Boss前
GameRole gameRole = new GameRole();
gameRole.initState();
gameRole.stateDisplay();

//保存进度
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setRoleStateMemento(gameRole.saveState());

System.out.println("------------大战Boss后------------");
//大战Boss时,损耗严重
gameRole.fight();
gameRole.stateDisplay();
System.out.println("------------恢复之前状态------------");
//恢复之前状态
gameRole.recoverState(roleStateCaretaker.getRoleStateMemento());
gameRole.stateDisplay();

}
}

分析:白箱备忘录模式是破坏封装性的。但是通过程序员自律,同样可以在一定程度上实现模式的大部分用意。

“黑箱”备忘录模式

备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。

将 RoleStateMemento 设为 GameRole 的内部类,从而将 RoleStateMemento 对象封装在 GameRole 里面;在外面提供一个标识接口 Memento 给 RoleStateCaretaker 及其他对象使用。这样 GameRole 类看到的是 RoleStateMemento 所有的接口,而RoleStateCaretaker 及其他对象看到的仅仅是标识接口 Memento 所暴露出来的接口,从而维护了封装型。类图如下:

代码如下:

窄接口Memento,这是一个标识接口,因此没有定义出任何的方法

1
2
java复制代码public interface Memento {
}

定义发起人类 GameRole,并在内部定义备忘录内部类 RoleStateMemento(该内部类设置为私有的)

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
java复制代码/游戏角色类
public class GameRole {
private int vit; //生命力
private int atk; //攻击力
private int def; //防御力

//初始化状态
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}

//战斗
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}

//保存角色状态
public Memento saveState() {
return new RoleStateMemento(vit, atk, def);
}

//回复角色状态
public void recoverState(Memento memento) {
RoleStateMemento roleStateMemento = (RoleStateMemento) memento;
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}

public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + def);

}

public int getVit() {
return vit;
}

public void setVit(int vit) {
this.vit = vit;
}

public int getAtk() {
return atk;
}

public void setAtk(int atk) {
this.atk = atk;
}

public int getDef() {
return def;
}

public void setDef(int def) {
this.def = def;
}

private class RoleStateMemento implements Memento {
private int vit;
private int atk;
private int def;

public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}

public int getVit() {
return vit;
}

public void setVit(int vit) {
this.vit = vit;
}

public int getAtk() {
return atk;
}

public void setAtk(int atk) {
this.atk = atk;
}

public int getDef() {
return def;
}

public void setDef(int def) {
this.def = def;
}
}
}

负责人角色类 RoleStateCaretaker 能够得到的备忘录对象是以 Memento 为接口的,由于这个接口仅仅是一个标识接口,因此负责人角色不可能改变这个备忘录对象的内容

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码//角色状态管理者类
public class RoleStateCaretaker {
private Memento memento;

public Memento getMemento() {
return memento;
}

public void setMemento(Memento memento) {
this.memento = memento;
}
}

客户端测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class Client {
public static void main(String[] args) {
System.out.println("------------大战Boss前------------");
//大战Boss前
GameRole gameRole = new GameRole();
gameRole.initState();
gameRole.stateDisplay();

//保存进度
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setMemento(gameRole.saveState());

System.out.println("------------大战Boss后------------");
//大战Boss时,损耗严重
gameRole.fight();
gameRole.stateDisplay();
System.out.println("------------恢复之前状态------------");
//恢复之前状态
gameRole.recoverState(roleStateCaretaker.getMemento());
gameRole.stateDisplay();
}
}

优缺点

1,优点:

  • 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
  • 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
  • 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。

2,缺点:

  • 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

使用场景

  • 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
  • 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,idea等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。

本文转载自: 掘金

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

小朋友, 好好学学lambda表达式吧!

发表于 2021-11-23

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

为什么要使用Lambda表达式

先看几段Java8以前经常会遇到的代码:

创建线程并启动

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// 创建线程
public class Worker implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
doWork();
}
}
}
// 启动线程
Worker w = new Worker();
new Thread(w).start();

比较数组

1
2
3
4
5
6
7
8
9
java复制代码// 定义一个比较器
public class LengthComparator implements Comparator<String> {
@Override
public int compare(String first, String second) {
return Integer.compare(first.length(), second.length());
}
}
//对字符数组进行比较
Arrays.sort(words, new LengthComparator());

给按钮添加单击事件

1
2
3
4
5
6
7
8
java复制代码public void onClick(Button button) {
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("button clicked.");
}
});
}

对于这三段代码,我们已经司空见惯了。

但他们的问题也很突出:就是噪声太多!想实现一个数组的比较功能,至少要写5行代码,但其中只有一行代码才是我们真正关注的!

Java复杂冗余的代码实现一直被程序员所诟病,好在随着JVM平台语言Scala的兴起以及函数式编程风格的风靡,让Oracle在Java的第8个系列版本中进行了革命性的变化,推出了一系列函数式编程风格的语法特性,比如Lambda表达式以及Stream。

如果采用Lambda表达式,上面三段代码的实现将会变得极为简洁。

创建线程并启动(采用Lambda版本)

1
2
3
4
5
java复制代码new Thread(() -> {
for (int i = 0; i < 100; i++) {
doWork();
}
}).start();

比较数组(采用Lambda版本)

1
java复制代码Arrays.sort(words, (first, second) -> Integer.compare(first.length(), second.length())

给按钮添加单击事件(采用Lambda版本)

1
java复制代码button.addActionListener((event) -> System.out.println("button clicked."));

怎么样?通过Lambda表达式,代码已经变得足够简洁,让你把关注点全部都放在业务代码上。

Lambda表达式的语法

格式:(参数) -> 表达式

其中:

  1. 参数可以为0-n个。如果有多个参数,以逗号(,)分割。如果有一个参数,括号()可以省去;如果没有参数,括号()也不能省去。[这就有点不够纯粹了,比scala还是差了点!],参数前可以加类型名,但由于自动类型推导功能,可以省去。
  2. 表达式可以是一行表达式,也可以是多条语句。如果是多条语句,需要包裹在大括号{}中。
  3. 表达式不需要显示执行返回结果,它会从上下文中自动推导。
    以下是一些例子:

一个参数

1
java复制代码event -> System.out.println("button clicked.")

多个参数

1
java复制代码(first, second) -> Integer.compare(first.length(), second.length()

0个参数

1
java复制代码() -> System.out.println("what are you nongshalei?")

表达式块

1
java复制代码() -> {for (int i = 0; i < 100; i++) {    doWork();}}

函数式接口

在Java8中新增加了一个注解: @FunctionalInterface,函数式接口。

什么是函数式接口呢?它包含了以下特征:

  • 接口中仅有一个抽象方法,但允许存在默认方法和静态方法。
  • @FunctionalInterface注解不是必须的,但建议最好加上,这样可以通过编译器来检查接口中是否仅存在一个抽象方法。

Lambda表达式的本质就是函数式接口的匿名实现。只是把原有的接口实现方式用一种更像函数式编程的语法表示出来。

Java8的java.util.function包已经内置了大量的函数式接口,如下所示:

函数式接口 参数类型 返回类型 方法名 描述
Supplier 无 T get 产生一个类型为T的数据
Consumer T void accept 消费一个类型为T的数据
BiConsumer<T,U> T,U void accept 消费类型为T和类型为U的数据
Function<T,R> T R apply 把参数类型为T的数据经过函数处理转换成类型为R的数据
BiFunction<T,U,R> T,U R apply 把参数类型为T和U的数据经过函数处理转换成类型为R的数据
UnaryOperator T T apply 对类型T进行了一元操作,仍返回类型T
BinaryOperator T,T T apply 对类型T进行了二元操作,仍返回类型T
Predicate T void test 对类型T进行函数处理,返回布尔值
BiPredicate<T,U> T,U void test 对类型T和U进行函数处理,返回布尔值

从中可以看出:

  • 内置的函数式接口主要分四类:Supplier, Consumer, Function,Predicate。Operator是Function的一种特例。
  • 除了Supplier没有提供二元参数以外(这和java不支持多个返回值有关),其他三类都提供了二元入参。

以下是一个综合的例子:

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
java复制代码public class FunctionalCase {
public static void main(String[] args) {
String words = "Hello, World";
String lowerWords = changeWords(words, String::toLowerCase);
System.out.println(lowerWords);

String upperWords = changeWords(words, String::toUpperCase);
System.out.println(upperWords);

int count = wordsToInt(words, String::length);
System.out.println(count);

isSatisfy(words, w -> w.contains("hello"));
String otherWords = appendWords(words, ()->{
List<String> allWords = Arrays.asList("+abc", "->efg");
return allWords.get(new Random().nextInt(2));
});
System.out.println(otherWords);
consumeWords(words, w -> System.out.println(w.split(",")[0]));
}
public static String changeWords(String words, UnaryOperator<String> func) {
return func.apply(words);
}
public static int wordsToInt(String words, Function<String, Integer> func) {
return func.apply(words);
}
public static void isSatisfy(String words, Predicate<String> func) {
if (func.test(words)) {
System.out.println("test pass");
} else {
System.out.println("test failed.");
}
}
public static String appendWords(String words, Supplier<String> func) {
return words + func.get();
}
public static void consumeWords(String words, Consumer<String> func) {
func.accept(words);
}
}

如果觉得这些内置函数式接口还不够用的话,还可以自定义自己的函数式接口,以满足更多的需求。

方法引用

如果Lambda表达式已经有实现的方法了,则可以用方法引用进行简化。
方法引用的语法如下:

  • 对象::实例方法
  • 类::静态方法
  • 类::实例方法

这样前面提到的Lambda表达式:

1
java复制代码event -> System.out.println(event)

则可以替换为:

1
java复制代码System.out::println

另一个例子:

1
java复制代码(x,y)->x.compareToIgnoreCase(y)

可以替换为:

1
arduino复制代码String::compareToIgnoreCase

注意:方法名后面是不能带参数的!
可以写成System.out::println,但不能写成System.out::println(“hello”)

如果能获取到本实例的this参数,则可以直接用this::实例方法进行访问,对于父类指定方法,用super::实例方法进行访问。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class Greeter {
public void greet() {
String lowcaseStr = changeWords("Hello,World", this::lowercase);
System.out.println(lowcaseStr);
}
public String lowercase(String word) {
return word.toLowerCase();
}
public String changeWords(String words, UnaryOperator<String> func) {
return func.apply(words);
}
}
class ConcurrentGreeter extends Greeter {
public void greet() {
Thread thread = new Thread(super::greet);
thread.start();
}
public static void main(String[] args) {
new ConcurrentGreeter().greet();
}
}

构造器引用

构造器引用和方法引用类似,只不过函数接口返回实例对象或者数组。
构造器引用的语法如下:

  • 类::new
  • 数组::new

举个例子:

1
2
3
java复制代码List<String> labels = Arrays.asList("button1", "button2");
Stream<Button> stream = labels.stream().map(Button::new);
List<Button> buttons = stream.collect(Collectors.toList());

其中的labels.stream().map(Button::new)相当于
labels.stream().map(label->new Button(label))

再看个数组类型的构造器引用的例子:

1
java复制代码Button[] buttons = stream.toArray(Button[]::new);

把Stream直接转成了数组类型,这里用Button[]::new来标示数组类型。

变量作用域

先看一段代码:

1
2
3
4
5
6
7
8
java复制代码public void repeatMsg(String text, int count) {
Runnable r = () -> {
for (int i = 0; i < count; i++) {
System.out.println(text);
Thread.yield();
}
};
}

一个lambda表达式一般由以下三部分组成:

  • 参数
  • 表达式
  • 自由变量

参数和表达式好理解。那自由变量是什么呢? 它就是在lambda表达式中引用的外部变量,比如上例中的text和count变量。

如果熟悉函数式编程的同学会发现,Lambda表达式其实就是”闭包”(closure)。只是Java8并未叫这个名字。
对于自由变量,如果Lambda表达式需要引用,是不允许发生修改的。

其实在Java的匿名内部类中,如果要引用外部变量,变量是需要声明为final的,虽然Lambda表达式的自由变量不用强制声明成final,但同样也是不允许修改的。

比如下面的代码:

1
2
3
4
5
6
7
8
java复制代码public void repeatMsg(String text, int count) {
Runnable r = () -> {
while (count > 0) {
count--; // 错误,不能修改外部变量的值
System.out.println(text);
}
};
}

另外,Lambda表达式中不允许声明一个和局部变量同名的参数或者局部变量。
比如下面的代码:

1
2
3
java复制代码Path first = Paths.get("/usr/bin");
Comparator<String> comp = (first, second) -> Integer.compare(first.length(), second.length());
// 错误,变量first已经被定义

接口中的默认方法

先说说为什么要在Java8接口中新增默认方法吧。

比如Collection接口的设计人员针对集合的遍历新增加了一个forEach()方法,用它可以更简洁的遍历集合。
比如:

1
java复制代码list.forEach(System.out::println());

但如果在接口中新增方法,按照传统的方法,Collection接口的自定义实现类都要实现forEach()方法,这对广大已有实现来说是无法接受的。

于是Java8的设计人员就想出了这个办法:在接口中新增加一个方法类型,叫默认方法,可以提供默认的方法实现,这样实现类如果不实现方法的话,可以默认使用默认方法中的实现。

一个使用例子:

1
2
3
4
5
6
7
java复制代码public interface Person {
long getId();

default String getName() {
return "jack";
}
}

默认方法的加入,可以替代之前经典的接口和抽象类的设计方式,统一把抽象方法和默认实现都放在一个接口中定义。这估计也是从Scala的Trait偷师来的技能吧。

接口中的静态方法

除了默认方法,Java8还支持在接口中定义静态方法以及实现。

比如Java8之前,对于Path接口,一般都会定义一个Paths的工具类,通过静态方法实现接口的辅助方法。

接口中有了静态方法就好办了, 统一在一个接口中搞定!虽然这看上去破坏了接口原有的设计思想。

1
2
3
4
5
java复制代码public interface Path{
public static Path get(String first, String... more) {
return FileSystem.getDefault().getPath(first, more);
}
}

这样Paths类就没什么意义了~

小结

使用Lambda表达式后可以大幅减少冗余的模板式代码,使把更多注意力放在业务逻辑上,而不是复制一堆重复代码, 除非你在一个用代码行数来衡量工作量的公司,你觉得呢?

本文转载自: 掘金

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

【死磕Java并发】-----JUC之AQS:同步状态的

发表于 2021-11-23

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


在前面提到过,AQS是构建Java同步组件的基础,我们期待它能够成为实现大部分同步需求的基础。AQS的设计模式采用的模板方法模式,子类通过继承的方式,实现它的抽象方法来管理同步状态,对于子类而言它并没有太多的活要做,AQS提供了大量的模板方法来实现同步,主要是分为三类:独占式获取和释放同步状态、共享式获取和释放同步状态、查询同步队列中的等待线程情况。自定义子类使用AQS提供的模板方法就可以实现自己的同步语义。

独占式

独占式,同一时刻仅有一个线程持有同步状态。

独占式同步状态获取

acquire(int arg)方法为AQS提供的模板方法,该方法为独占式获取同步状态,但是该方法对中断不敏感,也就是说由于线程获取同步状态失败加入到CLH同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。代码如下:

1
2
3
4
5
scss复制代码public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

各个方法定义如下:

  1. tryAcquire:去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。该方法自定义同步组件自己实现,该方法必须要保证线程安全的获取同步状态。
  2. addWaiter:如果tryAcquire返回FALSE(获取同步状态失败),则调用该方法将当前线程加入到CLH同步队列尾部。
  3. acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。
  4. selfInterrupt:产生一个中断。

acquireQueued方法为一个自旋的过程,也就是说当前线程(Node)进入同步队列后,就会进入一个自旋的过程,每个节点都会自省地观察,当条件满足,获取到同步状态后,就可以从这个自旋过程中退出,否则会一直执行下去。如下:

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
ini复制代码final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
//中断标志
boolean interrupted = false;
/*
* 自旋过程,其实就是一个死循环而已
*/
for (;;) {
//当前线程的前驱节点
final Node p = node.predecessor();
//当前线程的前驱节点是头结点,且同步状态成功
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//获取失败,线程等待--具体后面介绍
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

从上面代码中可以看到,当前线程会一直尝试获取同步状态,当然前提是只有其前驱节点为头结点才能够尝试获取同步状态,理由:

  1. 保持FIFO同步队列原则。
  2. 头节点释放同步状态后,将会唤醒其后继节点,后继节点被唤醒后需要检查自己是否为头节点。

acquire(int arg)方法流程图如下:

独占式获取响应中断

AQS提供了acquire(int arg)方法以供独占式获取同步状态,但是该方法对中断不响应,对线程进行中断操作后,该线程会依然位于CLH同步队列中等待着获取同步状态。为了响应中断,AQS提供了acquireInterruptibly(int arg)方法,该方法在等待获取同步状态时,如果当前线程被中断了,会立刻响应中断抛出异常InterruptedException。

1
2
3
4
5
6
7
java复制代码public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}

首先校验该线程是否已经中断了,如果是则抛出InterruptedException,否则执行tryAcquire(int arg)方法获取同步状态,如果获取成功,则直接返回,否则执行doAcquireInterruptibly(int arg)。doAcquireInterruptibly(int arg)定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ini复制代码private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

doAcquireInterruptibly(int arg)方法与acquire(int arg)方法仅有两个差别。1.方法声明抛出InterruptedException异常,2.在中断方法处不再是使用interrupted标志,而是直接抛出InterruptedException异常。

独占式超时获取

AQS除了提供上面两个方法外,还提供了一个增强版的方法:tryAcquireNanos(int arg,long nanos)。该方法为acquireInterruptibly方法的进一步增强,它除了响应中断外,还有超时控制。即如果当前线程没有在指定时间内获取同步状态,则会返回false,否则返回true。如下:

1
2
3
4
5
6
7
java复制代码public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}

tryAcquireNanos(int arg, long nanosTimeout)方法超时获取最终是在doAcquireNanos(int arg, long nanosTimeout)中实现的,如下:

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
java复制代码private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//nanosTimeout <= 0
if (nanosTimeout <= 0L)
return false;
//超时时间
final long deadline = System.nanoTime() + nanosTimeout;
//新增Node节点
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
//自旋
for (;;) {
final Node p = node.predecessor();
//获取同步状态成功
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
/*
* 获取失败,做超时、中断判断
*/
//重新计算需要休眠的时间
nanosTimeout = deadline - System.nanoTime();
//已经超时,返回false
if (nanosTimeout <= 0L)
return false;
//如果没有超时,则等待nanosTimeout纳秒
//注:该线程会直接从LockSupport.parkNanos中返回,
//LockSupport为JUC提供的一个阻塞和唤醒的工具类,后面做详细介绍
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
//线程是否已经中断了
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

针对超时控制,程序首先记录唤醒时间deadline ,deadline = System.nanoTime() + nanosTimeout(时间间隔)。如果获取同步状态失败,则需要计算出需要休眠的时间间隔nanosTimeout(= deadline - System.nanoTime()),如果nanosTimeout <= 0 表示已经超时了,返回false,如果大于spinForTimeoutThreshold(1000L)则需要休眠nanosTimeout ,如果nanosTimeout <= spinForTimeoutThreshold ,就不需要休眠了,直接进入快速自旋的过程。原因在于 spinForTimeoutThreshold 已经非常小了,非常短的时间等待无法做到十分精确,如果这时再次进行超时等待,相反会让nanosTimeout 的超时从整体上面表现得不是那么精确,所以在超时非常短的场景中,AQS会进行无条件的快速自旋。

整个流程如下:

独占式同步状态释放

当线程获取同步状态后,执行完相应逻辑后就需要释放同步状态。AQS提供了release(int arg)方法释放同步状态:

1
2
3
4
5
6
7
8
9
java复制代码public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

该方法同样是先调用自定义同步器自定义的tryRelease(int arg)方法来释放同步状态,释放成功后,会调用unparkSuccessor(Node node)方法唤醒后继节点(如何唤醒LZ后面介绍)。

这里稍微总结下:

在AQS中维护着一个FIFO的同步队列,当线程获取同步状态失败后,则会加入到这个CLH同步队列的对尾并一直保持着自旋。在CLH同步队列中的线程在自旋时会判断其前驱节点是否为首节点,如果为首节点则不断尝试获取同步状态,获取成功则退出CLH同步队列。当线程执行完逻辑后,会释放同步状态,释放后会唤醒其后继节点。

共享式

共享式与独占式的最主要区别在于同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。

共享式同步状态获取

AQS提供acquireShared(int arg)方法共享式获取同步状态:

1
2
3
4
5
arduino复制代码public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
//获取失败,自旋获取同步状态
doAcquireShared(arg);
}

从上面程序可以看出,方法首先是调用tryAcquireShared(int arg)方法尝试获取同步状态,如果获取失败则调用doAcquireShared(int arg)自旋方式获取同步状态,共享式获取同步状态的标志是返回 >= 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
ini复制代码private void doAcquireShared(int arg) {
/共享式节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//前驱节点
final Node p = node.predecessor();
//如果其前驱节点,获取同步状态
if (p == head) {
//尝试获取同步
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

tryAcquireShared(int arg)方法尝试获取同步状态,返回值为int,当其 >= 0 时,表示能够获取到同步状态,这个时候就可以从自旋过程中退出。

acquireShared(int arg)方法不响应中断,与独占式相似,AQS也提供了响应中断、超时的方法,分别是:acquireSharedInterruptibly(int arg)、tryAcquireSharedNanos(int arg,long nanos),这里就不做解释了。

共享式同步状态释放

获取同步状态后,需要调用release(int arg)方法释放同步状态,方法如下:

1
2
3
4
5
6
7
arduino复制代码public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

因为可能会存在多个线程同时进行释放同步状态资源,所以需要确保同步状态安全地成功释放,一般都是通过CAS和循环来完成的。

参考资料

  • Doug Lea:《Java并发编程实战》
  • 方腾飞:《Java并发编程的艺术》

本文转载自: 掘金

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

解决 HttpServletRequest 流数据不可重复读

发表于 2021-11-23

背景介绍

甲方客户的生产系统,有安全风险预警和安全事件快速溯源要求,需要做一套日志管理规范。

要求我们接入的系统,要对用户登录、注册、密码修改等重要场景,严格按照提供的格式,输出相应的日志。

后续通过filebeat对接,收集我们系统上的日志信息。

简单来说,就是应用系统,处理接口请求时,统一打印相应日志。

问题描述

成熟且常见的日志统一打印方案,就是使用AOP技术,自定义注解,在切面上使用环绕通知@Around,拦截请求,获取Controller类上方法的入参、出参即可。

奈何业务场景使用到的接口,以前的人在实现的时候,使用了如下方式:

1
2
3
4
java复制代码@RequestMapping(value = "/auth", method = { RequestMethod.POST, RequestMethod.GET, RequestMethod.OPTIONS })
public void auth(HttpServletRequest req, HttpServletResponse resp) {
authService.auth(req, resp);
}

把传参直接丢在 HttpServletRequest 中。

返回参数,又是采用 HttpServletResponse 输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public void printResult(HttpServletRequest req, HttpServletResponse resp,
String action, int code, String msg, Object result) {
PrintWriter p = null;
Ret ret = new Ret();
Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss")
.serializeNulls()
.create();
try {
p = resp.getWriter();
ret.setRspCode(code);
ret.setRspDesc(msg);
ret.setData(result);

p.write(gson.toJson(ret));
return;
} catch (Exception e) {
logger.error(e.getMessage());
} finally {
p.flush();
p.close();
}
}

不像平时熟练的做法,把具体入参和出参,用对象封装,直接放在方法上即可。

因为上面的做法,导致我们在拦截器中,想提前拦截请求获取传参,使用 request.getParameter() 等方法时,能拿到参数。
但是在具体接口业务流程中,再使用request.getParameter() 等方法,传入参数就获取不到了。

因为流只能被读一次。

因此就抛出一个问题:Request 和 Response 怎么重复读取?

解决方案

使用request.getParameter() 等方法,最终会调用getInputStream方法。

需要重写HttpServletRequestWrapper包装类,在调用getInputStream方法时,将流数据同时写到缓存。

后面想获取参数,直接读取缓存数据即可。

这样就可以实现Request的内容多次读取。

实现代码

封装request

自定义类 ContentCachingRequestWrapper

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
java复制代码import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

/**
*
* 重写 HttpServletRequestWrapper
*
* @Author: linzengrui
* @Date: 2021/11/22 15:33
*/
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {

private final byte[] body;

public ContentCachingRequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8))){
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
}
body = sb.toString().getBytes(StandardCharsets.UTF_8);
}

@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}

@Override
public ServletInputStream getInputStream() throws IOException {

final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);

return new ServletInputStream() {

@Override
public boolean isFinished() {
return false;
}

@Override
public boolean isReady() {
return false;
}

@Override
public void setReadListener(ReadListener readListener) {

}

@Override
public int read() throws IOException {
return inputStream.read();
}
};
}

public byte[] getBody() {
return body;
}
}

封装response

自定义类 ContentCachingResponseWrapper

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
java复制代码
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;

/**
*
* 重写 HttpServletResponseWrapper
*
* @Author: linzengrui
* @Date: 2021/11/22 19:45
*/
public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {

private ByteArrayOutputStream byteArrayOutputStream;
private ServletOutputStream servletOutputStream;
private PrintWriter printWriter;

public ContentCachingResponseWrapper(HttpServletResponse response) {
super(response);
byteArrayOutputStream = new ByteArrayOutputStream();
servletOutputStream = new ServletOutputStream() {
@Override
public boolean isReady() {
return false;
}

@Override
public void setWriteListener(WriteListener writeListener) {

}

@Override
public void write(int b) throws IOException {
byteArrayOutputStream.write(b);
}
};
printWriter = new PrintWriter(byteArrayOutputStream);
}

@Override
public PrintWriter getWriter() {
return printWriter;
}

@Override
public ServletOutputStream getOutputStream() throws IOException {
return servletOutputStream;
}

public byte[] toByteArray() {
return byteArrayOutputStream.toByteArray();
}

}

过滤器 Filter 拦截请求

拦截器 LogFilter 使用上面封装的包装类,即可获取传参。

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
java复制代码@Slf4j
@WebFilter(urlPatterns = "/*")
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 使用 重写 HttpServletRequestWrapper 的自定义包装类
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest)request);
// 使用 重写 HttpServletResponseWrapper 的自定义包装类
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);

// 只能执行一次获取流方法
try(ServletOutputStream outputStream = response.getOutputStream()){
// 获取传参
String requestParamJson = new String(requestWrapper.getBody());
log.info("requestParamJson --> {}", requestParamJson);

// 具体方法执行流程
chain.doFilter(requestWrapper, responseWrapper);

// 触发获取流操作后,可以从缓存多次拿数据
String respDataJson = new String(responseWrapper.toByteArray());
log.info("respDataJson <-- {}", respDataJson);

// TODO 写日志


// 需要重新写入内容,否则流无输出内容
outputStream.write(respDataJson.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}catch (Exception e){
e.printStackTrace();
}

}


@Override
public void destroy() {

}
}

springboot 启动类添加注解 @ServletComponentScan

注意:启动类要加上注解@ServletComponentScan识别上面注入的Filter。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码import org.springframework.boot.SpringApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@ServletComponentScan
@SpringBootApplication
public class SpringBootApplication {

public static void main(String[] args) {
SpringApplication.run(SpringBootApplication.class, args);
}

}

具体业务接口,原来的逻辑保持不变,仍然可以获取到入参。

本文转载自: 掘金

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

SpringBoot集成Swagger(五)动态配制Swag

发表于 2021-11-23

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


相关文章

Java随笔记:Java随笔记


前言

  • 一直都在讲如何过滤,不知道大家有没有考虑过一个问题。
  • 我们在工作中,也就是实际开发中,一般都是分环境的,总不能线上也搞个Swagger展示出来吧?
  • 一般都是开发环境才需要配制Swagger,方便前后端联调。
  • 那么,如何动态配制Swagger的开关呢?

动态Swagger开关

  • 前面讲过的Docket类还有印象吧?他里面有这么一个方法

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    kotlin复制代码​
     /**
      * Hook to externally control auto initialization of this swagger plugin instance.
      * Typically used if defer initialization.
      *
      * @param externallyConfiguredFlag - true to turn it on, false to turn it off
      * @return this Docket
      */
     public Docket enable(boolean externallyConfiguredFlag) {
       this.enabled = externallyConfiguredFlag;
       return this;
    }
  • 看下默认值

  • image-20211123215933420.png

  • 也就是说,我们前面不设置enable的话,默认是开启的。

  • 整合yml配制文件来动态开关

  • 关于如何配制动态yml配置文件可以点这里 如何将application配置文件玩出花样来?| SpringBoot系列(二)

  • 在此关于这个不在赘述

  • 首先建立两个yml文件

  • image-20211123221657396.png

  • 建立实体类SwaggerModel通过@ConfigurationProperties(prefix = "")接受配置文件的信息

+ 
1
2
3
4
5
6
less复制代码@Data
@Component
@ConfigurationProperties(prefix = "swagger")
public class SwaggerModel {
   private boolean enable;
}
  • SwaggerConfig改造
+ 
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
typescript复制代码@Configuration //配置类
@EnableSwagger2// 开启Swagger2的自动配置
public class SwaggerConfig {
​
   @Autowired
   private SwaggerModel swaggerModel;//读取配置文件的内容!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
​
   @Bean
   public Docket docket(){
       return new Docket(DocumentationType.SWAGGER_2)
              .apiInfo(apiInfo())
              .enable(swaggerModel.isEnable())//这里是改造重点!!!!!!!!!!!!!!!!!!!!!!!!!!!!
              .select()
              .paths(PathSelectors.any()))
              .build();
  }
​
   //配置文档信息
   private ApiInfo apiInfo() {
       Contact contact = new Contact("大鱼", "https://juejin.cn/user/2084329779387864/posts", "773530472@qq.com");
       return new ApiInfo(
               "大鱼随笔记", // 标题
               "Swagger的学习", // 描述
               "v1.0", // 版本
               "https://juejin.cn/user/2084329779387864/posts", // 组织链接
               contact, // 联系人信息
               "Apach 2.0 许可", // 许可
               "https://juejin.cn/user/2084329779387864/posts", // 许可连接
               new ArrayList<>()// 扩展
      );
  }
}
  • 到此我们先指定dev环境启动看看,理论上开启Swagger的
  • image-20211123222657278.png
  • image-20211123221943223.png
  • 结果如下:
  • image-20211123222101870.png
  • 完美!
  • 指定prod启动
  • image-20211123222252801.png
  • image-20211123222222480.png
  • 结果如下:
  • image-20211123222330027.png
  • 完美符合我们的要求!

总结

  • 其实这种玩法主要有三个知识点:
+ ①、动态配置文件
+ ②、`@ConfigurationProperties`注解读取配置文件的信息
+ ③、`Swagger`的开关
  • 以上都是个人所言,如有不对,欢迎指出。
  • 如果对您有帮助,希望给我点个赞点个关注呗!咱们明天继续Swagger的讲解!
  • 预告:Swagger实体的配置详解

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

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

Laravel 请求的生命周期介绍

发表于 2021-11-23

Laravel 是一个强大的PHP框架,当您学习laravel框架时,Laravel 请求生命周期是最好的起点。本文将介绍在Laravel中一个HTTP 请求从接收到响应之间发生了什么。对请求生命周期的深入研究将有助于我们理解 Laravel 结构。(基于Laravel 8)

请求生命周期有不同的术语,如自动加载器、内核、服务提供器、调度请求和路由等。一旦您详细了解了所有术语,您将对该框架有更多的理解,并且可以随心所欲地扩展不同的功能。

Laravel 请求生命周期

Laravel 请求生命周期概述

第一步

加载项目依赖,创建 Laravel 应用实例

Laravel 应用程序的所有请求的入口点都是 public/index.php 文件。所有请求都由你的 web 服务器(Apache/Nginx)配置定向到此文件。那个 index.php 文件不包含太多代码。相反,它是加载框架其余部分的起点。

1
2
3
4
php复制代码# 1、加载项目依赖
require __DIR__.'/../vendor/autoload.php';

$app = require_once __DIR__.'/../bootstrap/app.php';

该 index.php 文件将加载 Composer 生成的自动加载器定义,然后从 bootstrap/app.php 中检索 Laravel 应用程序的实例。

bootstrap/app.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
php复制代码<?php

# 2、创建应用实例
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);

# 3、完成内核绑定
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);

$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);

$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);

return $app;

之后,它将引导 Laravel 框架使用并生成应用程序实例。

public/index.php:

1
2
3
4
5
6
7
8
9
10
11
php复制代码# 4、接收请求并响应
$kernel = $app->make(Kernel::class);

// 处理请求
$response = tap($kernel->handle(
// 创建请求实例
$request = Request::capture()
// 发送响应
))->send();

$kernel->terminate($request, $response);

一旦应用程序实例生成,传入请求将由内核处理。

HTTP 或 Console 内核

接下来,传入请求被发送到 HTTP 内核还是 Console 内核,具体取决于进入应用的请求类型。这两个内核充当所有请求流经的中心位置。现在,让我们只关注 HTTP 内核,它位于 app/Http/Kernel.php 中。

HTTP 内核扩展了 Illuminate\Foundation\Http\kernel 类,该类定义了一个将在执行请求之前运行的 bootstrappers 数组。这些引导程序用来配置异常处理、配置日志、检测应用程序环境 ,并执行在实际处理请求之前需要完成的其他任务。通常情况下,你不需要在意这些配置。

HTTP 内核还定义了一个 HTTP 中间件列表,所有请求在被应用程序处理之前必须通过这些中间件。这些中间件处理 HTTP 会话的读写、确定应用程序是否处于维护模式、验证 CSRF 令牌等。我们接下来会做详细的讨论。

HTTP 内核的 handle 方法的签名非常简单:它接收 Request 接口并返回 Response 接口。把内核想象成一个代表整个应用程序的大黑匣子。向它提供 HTTP 请求,它将返回 HTTP 响应。

通过配置中间件和其他功能,HTTP 内核还加载服务提供者。

服务提供器

最重要的内核引导操作之一是为应用程序加载 service providers。应用程序的所有服务提供程序都在 config/app.php 中的 providers 数组。

Laravel 将遍历这个提供者列表并实例化它们中的每一个。实例化提供程序后,将对所有提供程序调用 register 方法。然后,一旦注册了所有提供程序,就会对每个提供程序调用 boot 方法。

服务提供者负责引导框架的所有不同组件,如数据库、队列、验证和路由组件。基本上,Laravel 提供的每个主要功能都是由服务提供商引导和配置的。由于它们引导和配置框架提供的许多特性,服务提供者是整个 Laravel 引导过程中最重要的部分。

您可能想知道,为什么在对任何服务提供者调用 boot 方法之前都要调用每个服务提供者的 register 方法。答案很简单。通过首先调用每个服务提供程序的 register 方法,服务提供者可能依赖于在执行 boot 方法时注册并可用的每个容器绑定。

服务提供者是引导 Laravel 应用程序的关键。应用程序实例被创建,服务提供者被注册,请求被交给引导的应用程序。真的就是这么简单!

牢牢掌握 Laravel 应用程序如何通过服务提供商构建和引导是非常有价值的。您的应用程序的默认服务提供者存储在该app/Providers目录中。

默认情况下,AppServiceProvider是空的。此程序是添加应用程序自己的引导和服务容器绑定的好地方。对于大型应用程序,您可能希望创建多个服务提供者,每个服务提供者为您的应用程序使用的特定服务提供更精细的引导。

一旦应用程序被引导并且所有服务提供者都被注册和引导,请求将被移交给路由器进行调度。

路由

应用程序中最重要的服务提供者之一是 App\Providers\RouteServiceProvider。此服务提供程序加载应用程序的 routes 目录中包含的路由文件。

路由器将请求发送到路由或控制器,并运行任何路由特定的中间件。

中间件为过滤或检查进入应用程序的 HTTP 请求提供了一种方便的机制。例如,Laravel 包含一个这样的中间件,用于验证应用程序的用户是否经过身份验证。如果用户未通过身份验证,中间件将用户重定向到登录页。但是,如果用户经过身份验证,中间件将允许请求进一步进入应用程序。一些中间件被分配给应用程序中的所有路由,比如那些在 HTTP 内核的 $middleware 属性中定义的路由,而一些只被分配给特定的路由或路由组。您可以通过阅读完整的 中间件 文档来了解更多关于中间件的信息。

如果请求通过了所有匹配路由分配的中间件,则将 HTTP 请求定向到控制器或通过省略控制器直接返回视图或响应

控制器

控制器 app/Http/Controllers/ 执行特定操作并将数据发送到视图。

视图

视图 resources/views/ 适当地格式化数据,提供 HTTP 响应。

最后

一旦路由或控制器方法返回一个响应,该响应将通过路由的中间件返回,从而使应用程序有机会修改或检查传出的响应。

通常,不会只从路由操作中返回简单的字符串或数组。而是返回完整的 Illuminate\Http\Response 实例或视图。

Response 实例派生自 Symfony\Component\Http\Foundation\Response 类,它提供了许多构造 HTTP 响应的方法。

最后,一旦响应通过中间件传回,HTTP 内核的 handle 方法将返回响应对象,并且index.php文件对返回的响应调用 send 方法。send 方法将响应内容发送到用户的 web 浏览器。

至此,我们已经完成了整个 Laravel 请求生命周期的所有步骤!

转自我的博客:www.muouseo.com/article/kq1…

本文转载自: 掘金

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

Spring Data JPA-属性详解 JdbcPrope

发表于 2021-11-23

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

最近查了一个 spring data jpa 的问题,其实也不能算是框架层面的问题,准确说是配置;因为之前没有怎么使用过 spring data jpa(更多是 mybatis 或者自研 orm 组件),所以期望通过本篇来记录下 spring data jpa 的配置。

本篇从配置源码开始,先直观的了解 spring data jpa 自己有哪些配置,然后会结合具体的 case 来描述各个配置的作用;同时也对类似 spring.datasource.* 的配置也做简单介绍,不要再傻傻分不清了。

分为 一、二 和 综合实践三部分,本篇为第一部分 jdbc 配置相关

jdbc 配置和 jpa 配置

在没有使用之前,一直会认为 datasource 的配置,如:url, class-drive-name 等也是在 spring data jpa 下,看起来还不是。换个角度想了下,url, class-drive-name 这些属于 jdbc 的配置,并非是 orm 的配置,所以也就理所应当了。下面分别看看 JpaProperties 和 HibernateProperties 以及 jdbc 的配置。

jdbc 配置

在 spring 的 jdbc 代码模块中,jdbc 的配置包括两个:

  • JdbcProperties: prefix 为 spring.jdbc.*
  • DataSourceProperties: prefix 为 spring.datasource.*

JdbcProperties

下面是 JdbcProperties,对于这几个配置,着实看起来有点陌生

1
2
3
4
5
6
7
8
java复制代码@ConfigurationProperties(prefix = "spring.jdbc")
public class JdbcProperties {
private final Template template = new Template();
public static class Template {
private int fetchSize = -1;
private int maxRows = -1;
@DurationUnit(ChronoUnit.SECONDS)
private Duration queryTimeout;

JdbcProperties 配置通过代码分析来看,目前只有在 JdbcTemplate 中使用了;也就是说,如果你的项目中没有使用 JdbcTemplate 的话,那 spring.jdbc.template.* 是无用的。抛个问题:如果使用 JdbcTemplate, 并且将 queryTimeout 配置成 -1 是不是一定会超时?为 0 呢?答案肯定是 否;

jdbc 的这几个参数从代码看,最终都是为底层 Statement 服务的,这里不具体介绍执行过程及原理;这里放一个之前看到的一篇查询超时问题的排查,以供各位理解:关于jdbc的setQueryTimeout() 的bug和Query execution was interrupted 的调查

  • fetchSize: 从数据库和结果集查询时,每次拉取指定 fetchSize 大小的数据,循环去取,直到取完。
  • maxRows:底层 Statement 对象生成的所有 ResultSet 对象可以包含的最大行数限制,最大以后的数据会被丢掉;这个参数设置应该是为了避免在结果集非常大的时候导致内存扛不住。

DataSourceProperties

这个比较容易理解,就是数据源配置,常见的 url, driverClassName 都是在这里配置的。

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
java复制代码@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
private ClassLoader classLoader;
// datasource 名字,如果使用 embedded database,默认名是 "testdb"
private String name;
// 是否要随机创建一个 datasource name
private boolean generateUniqueName = true;
// 具体 connection pool 实现类的全限定名
private Class<? extends DataSource> type;
// 具体 JDBC driver 实现类的全限定名
private String driverClassName;
/**
* JDBC URL of the database.
*/
private String url;

/**
* Login username of the database.
*/
private String username;

/**
* Login password of the database.
*/
private String password;

/**
* 数据源的JNDI位置,如果使用这个,url,username 这些就不需要了。(没用过)
*/
private String jndiName;
// 初始化模式,比如是否使用可用的 DDL and DML 脚本
private DataSourceInitializationMode initializationMode = DataSourceInitializationMode.EMBEDDED;

/**
* 在DDL 或 DML脚本中使用的平台 (such as schema-${platform}.sql or
* data-${platform}.sql).
*/
private String platform = "all";

// Schema (DDL) 脚本的资源位置.
private List<String> schema;
private String schemaUsername;
private String schemaPassword;

// Data (DML) 脚本资源位置.
private List<String> data;
private String dataUsername;
private String dataPassword;

// 是否在初始化数据库时发生错误时停止。
private boolean continueOnError = false;

// SQL初始化脚本中的语句分隔符。
private String separator = ";";

// 编码格式
private Charset sqlScriptEncoding;

一般项目中,常见的配置大概如下,应该是对于任何 spring 配置数据源来说,这个都是必不可少的:

1
2
3
4
properties复制代码spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=
spring.datasource.url=jdbc:mysql://localhost:3306/glmapper?createDatabaseIfNotExist=true&xxx=xxx

配置初始化的 DDL 和 DML

1
2
properties复制代码spring.datasource.schema=schema.sql
spring.datasource.data=data.sql

如果配置中指定了 platform

1
properties复制代码spring.datasource.platform=test

那么需要将 配置初始化的 DDL 和 DML 修改为

1
2
properties复制代码spring.datasource.schema=schema-test.sql
spring.datasource.data=data-test.sql

否则会出现找不到 resource 的报错,如下:

1
2
3
bash复制代码Invalid value 'schema.sql' for configuration property 'spring.datasource.schema' (originating from 'class path resource [application.properties] - 7:26'). Validation failed for the following reason:

The specified resource does not exist.

小结

本篇主要介绍 Spring Data JPA 配置部分关于 jdbc 的配置,如有问题欢迎指正。

本文转载自: 掘金

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

1…214215216…956

开发者博客

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