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

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


  • 首页

  • 归档

  • 搜索

JWT如何在OpenFeign调用中进行令牌中继

发表于 2021-10-26

小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

下方留言,掘金官方送周边啦 博文中哪里写的不好的欢迎评论区指出 掘金官方抽送100个周边啦

在Spring Cloud微服务开发中使用Feign时需要处理令牌中继的问题,只有令牌中继才能在调用链中保证用户认证信息的传递,实现将A服务中的用户认证信息通过Feign隐式传递给B服务。今天就来分享一下如何在Feign中实现令牌中继。

令牌中继

令牌中继(Token Relay)是比较正式的说法,说白了就是让Token令牌在服务间传递下去以保证资源服务器能够正确地对调用方进行资源鉴权。客户端通过网关携带JWT访问了A服务,A服务对JWT进行了校验解析,A服务调用B服务时,可能B服务也需要对JWT进行校验解析。举个例子,查询我的订单以及我订单的物流信息,订单服务通过JWT能够获得我的userId,如果不中继令牌需要显式把userId在传递给物流信息服务,甚至有时候下游服务还有权限的问题要处理,所以令牌中继是非常必要的。

令牌难道不能在Feign自动中继吗?

如果我们携带Token去访问A服务,A服务肯定能够鉴权,但是A服务又通过Feign调用B服务,这时候A的令牌是无法直接传递给B服务的。

这里来简单说下原因,服务间的调用通过Feign接口来进行。在调用方通常我们编写类似下面的Feign接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@FeignClient(name = "foo-service",fallback = FooClient.Fallback.class)
public interface FooClient {
@GetMapping("/foo/bar")
Rest<Map<String, String>> bar();

@Component
class Fallback implements FooClient {
@Override
public Rest<Map<String, String>> bar() {
return RestBody.fallback();
}
}
}

当我们调用Feign接口后,会通过动态代理来生成该接口的代理类供我们调用。如果我们不打开熔断我们可以从Spring Security提供SecurityContext对象中提取到资源服务器的认证对象JwtAuthenticationToken,它包含了JWT令牌然后我们可以通过实现Feign的拦截器接口RequestInterceptor把Token放在请求头中,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/**
* 需要注入Spring IoC
**/
static class BearerTokenRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
final String authorization = HttpHeaders.AUTHORIZATION;
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication instanceof JwtAuthenticationToken){
JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
String tokenValue = jwtAuthenticationToken.getToken().getTokenValue();
template.header(authorization,"Bearer "+tokenValue);
}
}
}

如果我们不开启熔断这样搞问题不大,为了防止调用链雪崩服务熔断基本没有不打开的。这时候从SecurityContextHolder就无法获取到Authentication了。因为这时Feign调用是在调用方的调用线程下又开启了一个子线程中进行的。由于我使用的熔断组件是Resilience4J,对应的线程执行源码是Resilience4JCircuitBreaker下面的片段:

1
java复制代码	Supplier<Future<T>> futureSupplier = () -> executorService.submit(toRun::get);

SecurityContextHolder保存信息是默认是通过ThreadLocal实现的,我们都知道这个是不能跨线程的,而Feign的拦截器这时恰恰在子线程中,因此开启了熔断功能(circuitBreaker)的Feign无法直接进行令牌中继。

熔断组件有过时的Hystrix、Resilience4J、还有阿里的哨兵Sentinel,它们的机制可能有小小的不同。

实现令牌中继

虽然直接不能实现令牌中继,但是我从中还是找到了一些信息。在Feign接口代理的处理器FeignCircuitBreakerInvocationHandler中发现了下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码private Supplier<Object> asSupplier(final Method method, final Object[] args) {
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
return () -> {
try {
RequestContextHolder.setRequestAttributes(requestAttributes);
return this.dispatch.get(method).invoke(args);
}
catch (RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
finally {
RequestContextHolder.resetRequestAttributes();
}
};
}

这是Feign代理类的执行代码,我们可以看到在执行前:

1
java复制代码		final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

这里是获得调用线程中请求的信息,包含了ServletHttpRequest、ServletHttpResponse等信息。紧接着又在lambda代码中把这些信息又Setter了进去:

1
java复制代码	RequestContextHolder.setRequestAttributes(requestAttributes);

如果这是一个线程中进行的简直就是吃饱了撑的,事实上Supplier返回值是在另一个线程中执行的。这样做的目的就是为了跨线程保存一些请求的元数据。

InheritableThreadLocal

RequestContextHolder 是如何做到跨线程了传递数据的呢?

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

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");

private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<>("Request context");
// 省略
}

RequestContextHolder 维护了两个容器,一个是不能跨线程的ThreadLocal,一个是实现了InheritableThreadLocal的NamedInheritableThreadLocal。InheritableThreadLocal是可以把父线程的数据传递到子线程的,基于这个原理RequestContextHolder把调用方的请求信息带进了子线程,借助于这个原理就能实现令牌中继了。

实现令牌中继

把最开始的Feign拦截器代码改动了一下就实现了令牌的中继:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码    /**
* 令牌中继
*/
static class BearerTokenRequestInterceptor implements RequestInterceptor {
private static final Pattern BEARER_TOKEN_HEADER_PATTERN = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
Pattern.CASE_INSENSITIVE);

@Override
public void apply(RequestTemplate template) {
final String authorization = HttpHeaders.AUTHORIZATION;
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (Objects.nonNull(requestAttributes)) {
String authorizationHeader = requestAttributes.getRequest().getHeader(HttpHeaders.AUTHORIZATION);
Matcher matcher = BEARER_TOKEN_HEADER_PATTERN.matcher(authorizationHeader);
if (matcher.matches()) {
// 清除token头 避免传染
template.header(authorization);
template.header(authorization, authorizationHeader);
}
}
}
}

这样当你调用FooClient.bar()时,在foo-service中资源服务器(OAuth2 Resource Server)也可以获得调用方的令牌,进而获得用户的信息来处理资源权限和业务。

不要忘记将这个拦截器注入Spring IoC。

总结

微服务令牌中继是非常重要的,保证了用户状态在调用链路的传递。而且这也是微服务的难点。今天借助于Feign的一些特性和ThreadLocal的特性实现了令牌中继供大家参考。原创不易,请大家多多点赞、评论。

本文转载自: 掘金

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

Java程序员必须知道的Java11特性

发表于 2021-10-26

小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

Java 11是自Java 8以来的又一个LTS版本,是目前全球使用最多的LTS版本之一。今天我们接着在Java 9 到 Java 17系列文章中来认识针对普通开发者的Java 11。

字符串API增强

在Java 11中,针对String的操作进一步得到加强。避免我们在很常见的场景中引入额外的、复杂的API。

isBlank()

用来判断字符串是不是空字符""或者trim()之后(" ")为空字符:

1
2
3
java复制代码 String blankStr = "    ";
 // true
 boolean trueVal = blankStr.isBlank();

lines()

将一个字符串按照行终止符(换行符\n或者回车符\r)进行分割,并将分割为Stream流:

1
2
3
4
5
6
7
java复制代码         String newStr = "Hello Java 11 \n felord.cn \r 2021-09-28";
 ​
         Stream<String> lines = newStr.lines();
         lines.forEach(System.out::println);
 //       Hello Java 11
 //       felord.cn
 //       2021-09-28

strip()

去除字符串前后的“全角和半角”空白字符:

1
2
3
4
5
6
7
java复制代码 String str = "HELLO\u3000";
 // str = 6
 System.out.println("str = " + str.length());
 // trim = 6
 System.out.println("trim = " + str.trim().length());
 // strip = 5
 System.out.println("strip = " + str.strip().length());

这不由得想起来trim()方法,从上面也看出来了差别,trim()只能去除半角空白符。

strip()方法还有两个变种,stripLeading()用来去除前面的全角半角空白符;stripTrailing()用来去除尾部的全角半角空白符。

repeat(n)

按照给定的次数重复串联字符串的内容:

1
2
3
4
5
6
7
java复制代码 String str = "HELLO";
 // 空字符
 String empty = str.repeat(0);
 // HELLO
 String repeatOne = str.repeat(1);
 // HELLOHELLO
 String repeatTwo = str.repeat(2);

集合转对应类型的数组

之前想集合转对应的数组很麻烦,要么用迭代;要么用Stream流,现在你可以这样:

1
2
3
java复制代码         List<String> sampleList = Arrays.asList("felord.cn", "java 11");
         // array = {"felord.cn", "java 11"};
         String[] array = sampleList.toArray(String[]::new);

断言取反

java.util.function.Predicate<T>是我们很常用的断言谓词函数。在以前取反我们得借助于!符号,到了Java 11我们可以借助于其静态方法not来实现,这样语义就更加清晰了:

1
2
3
4
5
6
7
8
java复制代码         List<String> sampleList = Arrays.asList("felord.cn", "java 11","jack");
         // [jack]
         List<String> result = sampleList.stream()
                 // 过滤以j开头的字符串
                .filter(s -> s.startsWith("j"))
                 // 同时不包含11的字符串
                .filter(Predicate.not(s -> s.contains("11")))
                .collect(Collectors.toList());

其实Predicate<T>在最初版本还提供了一个取反的默认方法:

1
2
3
scss复制代码   default Predicate<T> negate() {
         return (t) -> !test(t);
    }

这个我在往期文章中也使用过它来做组合校验,这两个方法的场景是不一样的。

var可以用于修饰Lambda局部变量

在Java 10中引入的var来进行类型推断。在Java 10中它不能用于修饰Lambda表达式的入参,其实对于一个Lambda表达式来说它入参的类型其实是可以根据上下文推断出来的。拿上面的例子来说,s -> s.startsWith("j")中的s肯定是字符串类型,因此在Java 11中var可以用于修饰Lambda局部变量:

1
2
3
4
5
6
java复制代码         List<String> result = sampleList.stream()
                 // 过滤以j开头的字符串
                .filter((@NotNull var s) -> s.startsWith("j"))
                 // 同时不包含11的字符串
                .filter(Predicate.not((@NotNull var s) -> s.contains("11")))
                .collect(Collectors.toList());

如果我们不声明var就没有办法为输入参数添加@NotNull注解。

文件中读写字符串内容更方便

Java 11中可以更轻松地从文件中读取和写入字符串内容了,我们可以通过Files工具类提供的新的静态方法readString和writeString分别进行读写文件的字符串内容,放在之前老麻烦了,特别是对IO流不熟悉的同学来说。现在简单几行就搞定了:

1
2
3
4
5
java复制代码String dir= "C://yourDir";
// 写入文件
Path path = Files.writeString(Files.createTempFile(dir, "hello", ".txt"), "hello java 11");
// 读取文件
String fileContent = Files.readString(path);

嵌套类的访问控制规则

在Java 11之前,内部嵌套类访问外部类的私有属性和方法是可行的:

1
2
3
4
5
6
7
8
9
java复制代码public class Outer {
private int outerInt;

class Inner {
public void printOuterField() {
System.out.println("Outer field = " + outerInt);
}
}
}

但是如果你通过反射API实现内部类访问外部类的私有属性和方法就会抛出IllegalStateException异常。Java 11 修复了反射不能访问的问题.

JVM 访问规则不允许嵌套类之间进行私有访问。我们能通过常规方式可以访问是因为 JVM 在编译时为我们隐式地创建了桥接方法。Java 11 中引入了两个新的属性:一个叫做 NestMembers 的属性,用于标识其它已知的静态 nest 成员;另外一个是每个 nest 成员都包含的 NestHost 属性,用于标识出它的 nest 宿主类。在编译期就映射了双方的寄宿关系,不再需要桥接了。

HttpClient支持HTTP2

HttpClient到了Java 11后开始支持HTTP2,底层进行了大幅度的优化,并且现在完全支持异步非阻塞。

HttpClient 的包名由 jdk.incubator.http 改为 java.net.http。

其它

Java 11 中,还有一些其它方面的特性和优化,比如引入了ZGC,支持支持 TLS 1.3 协议,引入了动态调用(invokedynamic)机制,另外原来商业版的JFR也进行了开源集成等等。在年初的Java生态调查数据显示Java 11的用户数量大幅增长,成为了主流版本选择之一。

下方留言,掘金官方送周边啦 博文中哪里写的不好的欢迎评论区指出 掘金官方抽送100个周边啦

本文转载自: 掘金

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

springboot集成本地kafka

发表于 2021-10-26

话不多说,直接开始。

一、本地安装kafka并运行。

首先打开kafka官方下载地址kafka.apache.org/downloads ,下载kafka到本地。
版本选择的话,一般比springboot版本大一版即可,比如我使用的是springboot2.2.2版本,下载2.3.1版本,使用spring-kafka版本为2.3.4。(不要下载带src的源码压缩文件)

下载成功解压,在cmd进入kafka所在文件位置,比如我的kafka路径为D:\devtools\kafka_2.12-2.3.1。

image.png
输入命令:.\bin\windows\zookeeper-server-start.bat .\config\zookeeper.properties

zookeeper运行成功,同理,进入相同路径,输入命令:.\bin\windows\kafka-server-start.bat .\config\server.properties

此时,kafka服务启动成功。

二、springboot集成kafka。

1. pom依赖

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
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

我这里spring-kafka默认是 2.3.4release。

springboot版本为2.2.2release

image.png

2. 配置及代码

配置有两种配置方法,一种是springboot中application配置文件配置,另一种是java代码配置。选择其一即可。

(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
yaml复制代码# 应用端口号
server:
port: 28099
# Kafka配置
spring:
kafka:
# 指定kafka 代理地址,可以多个
bootstrap-servers: 127.0.0.1:9092

producer:
retries: 0
# 每次批量发送消息的数量
batch-size: 16384
# 缓存容量
buffer-memory: 33554432
# 指定消息key和消息体的编解码方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
# 指定默认消费者group id
group-id: consumer-tutorial
auto-commit-interval: 100
auto-offset-reset: earliest
enable-auto-commit: true
# 指定消息key和消息体的编解码方式
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
# 指定listener 容器中的线程数,用于提高并发量
listener:
missing-topics-fatal: false
concurrency: 3

(2). 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
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
typescript复制代码package com.xuegao.kafkaproject.config;

import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.KafkaListenerContainerFactory;
import org.springframework.kafka.config.TopicBuilder;
import org.springframework.kafka.core.*;
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
* @author xg
* @Date: 2021/10/21 19:22
*/
@Configuration
@EnableKafka
public class KafkaConfig {

@Bean
ConcurrentKafkaListenerContainerFactory<Integer, String>
kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}

@Bean
public ConsumerFactory<Integer, String> consumerFactory() {
return new DefaultKafkaConsumerFactory<>(consumerConfigs());
}

@Bean
public Map<String, Object> consumerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "default-topic-group");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
return props;
}


@Bean
public ProducerFactory<String, Object> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}

@Bean
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
return props;
}

@Bean
public KafkaTemplate<String, Object> kafkaTemplate() {
return new KafkaTemplate<String, Object>(producerFactory());
}


@Bean
public NewTopic initialSendMsgTopic() {
return new NewTopic("kafka.topic",8, (short) 2 );
}

@Bean
public NewTopic topic1() {
return TopicBuilder.name("my-topic")
.partitions(10)
.replicas(3)
.compact()
.build();
}
}

注:使用java配置,使用的topic需要在配置文件里注册,如上,否则运行会报错,而配置文件中有missing-topics-fatal: false,使用未注册为bean的topic不会报错。

生产者和消费者方法

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
typescript复制代码@Resource
private KafkaTemplate<String, Object> kafkaTemplate;

/**
* kafka 发送消息
*
* @param obj 消息对象
*/
public void send(T obj) {
String jsonObj = JSON.toJSONString(obj);
logger.info("------------ message = {}", jsonObj);

//发送消息
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send("kafka.topic", jsonObj);
future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
@Override
public void onFailure(Throwable throwable) {
logger.info("Produce: The message failed to be sent:" + throwable.getMessage());
}

@Override
public void onSuccess(SendResult<String, Object> stringObjectSendResult) {
//TODO 业务处理
logger.info("Produce: The message was sent successfully:");
logger.info("Produce: _+_+_+_+_+_+_+ result: " + stringObjectSendResult.toString());
}
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
less复制代码/**
* 监听kafka.tut 的topic,不做其他业务
*
* @param record
* @param topic topic
*/
@KafkaListener(id = "tutest", topics = "kafka.topic")
public void listen(ConsumerRecord<?, ?> record, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
Optional<?> kafkaMessage = Optional.ofNullable(record.value());

if (kafkaMessage.isPresent()) {
Object message = kafkaMessage.get();

logger.info("Receive: +++++++++++++++ Topic:" + topic);
logger.info("Receive: +++++++++++++++ Record:" + record);
logger.info("Receive: +++++++++++++++ Message:" + message);
}
}

最后,在写个测试controller,使用网页访问接口调用生产者方法,访问成功之后,控制台会出现消费者打印的日志。

祝你好运,有异常百度即可。

本文转载自: 掘金

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

【算法学习】1431 拥有最多糖果的孩子(各种语言击败10

发表于 2021-10-26
  • 本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

非常感谢你阅读本文~
欢迎【👍点赞】【⭐收藏】【📝评论】~
放弃不难,但坚持一定很酷~
希望我们大家都能每天进步一点点~
本文由 二当家的白帽子 https://juejin.cn/user/2771185768884824/posts 博客原创~


  1. 拥有最多糖果的孩子:

给你一个数组 candies 和一个整数 extraCandies ,其中 candies[i] 代表第 i 个孩子拥有的糖果数目。

对每一个孩子,检查是否存在一种方案,将额外的 extraCandies 个糖果分配给孩子们之后,此孩子有 最多 的糖果。注意,允许有多个孩子同时拥有 最多 的糖果数目。

样例 1

1
2
3
4
5
6
7
8
9
10
arduino复制代码输入:
candies = [2,3,5,1,3], extraCandies = 3
输出:
[true,true,true,false,true]
解释:
孩子 1 有 2 个糖果,如果他得到所有额外的糖果(3个),那么他总共有 5 个糖果,他将成为拥有最多糖果的孩子。
孩子 2 有 3 个糖果,如果他得到至少 2 个额外糖果,那么他将成为拥有最多糖果的孩子。
孩子 3 有 5 个糖果,他已经是拥有最多糖果的孩子。
孩子 4 有 1 个糖果,即使他得到所有额外的糖果,他也只有 4 个糖果,无法成为拥有糖果最多的孩子。
孩子 5 有 3 个糖果,如果他得到至少 2 个额外糖果,那么他将成为拥有最多糖果的孩子。

样例 2

1
2
3
4
5
6
arduino复制代码输入:
candies = [4,2,1,1,2], extraCandies = 1
输出:
[true,false,false,false,false]
解释:
只有 1 个额外糖果,所以不管额外糖果给谁,只有孩子 1 可以成为拥有糖果最多的孩子。

样例 3

1
2
3
4
arduino复制代码输入:
candies = [12,1,12], extraCandies = 10
输出:
[true,false,true]

提示

  • 2 <= candies.length <= 100
  • 1 <= candies[i] <= 100
  • 1 <= extraCandies <= 50

分析

题目问有没有一种方案让某个孩子拥有最多的糖果?

  • 在有额外糖果的情况下想要让一个孩子拥有最多糖果的方案可能很多(这个孩子可能已经拥有最多糖果,那给不给他额外糖果都可以;或者这个孩子已经有很多的糖果,只用给他几个他就最多了,多出来的还可以给别的孩子)
  • 但是我们最多让一个孩子有多少糖果是只有唯一结果的,就是把所有额外糖果都给他,如果我们尽全力帮他,他都不能是最多的,那就是没办法。

题解

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码class Solution {
public List<Boolean> kidsWithCandies(int[] candies, int extraCandies) {
// 查找最多有多少糖果
int max = 0;
for (int c : candies) {
max = Math.max(max, c);
}

// 假设额外的糖果都给他
List<Boolean> ans = new ArrayList<>(candies.length);
for (int c : candies) {
ans.add(c + extraCandies >= max);
}

return ans;
}
}

c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c复制代码/**
* Note: The returned array must be malloced, assume caller calls free().
*/
bool* kidsWithCandies(int* candies, int candiesSize, int extraCandies, int* returnSize){
// 查找最多有多少糖果
*returnSize = candiesSize;
int max = 0;
for (int i = 0; i < candiesSize; ++i) {
if (candies[i] > max) {
max = candies[i];
}
}

// 假设额外的糖果都给他
bool *ans = (bool *) malloc(sizeof(bool) * candiesSize);
for (int i = 0; i < candiesSize; ++i) {
ans[i] = candies[i] + extraCandies >= max;
}

return ans;
}

c++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cpp复制代码class Solution {
public:
vector<bool> kidsWithCandies(vector<int>& candies, int extraCandies) {
// 查找最多有多少糖果
int max = *max_element(candies.begin(), candies.end());

// 假设额外的糖果都给他
vector<bool> ans;
for (auto c : candies) {
ans.push_back(c + extraCandies >= max);
}

return ans;
}
};

python

1
2
3
4
5
6
7
python复制代码class Solution:
def kidsWithCandies(self, candies: List[int], extraCandies: int) -> List[bool]:
# 查找最多有多少糖果
maxCandies = max(candies)
# 假设额外的糖果都给他
ans = [c + extraCandies >= maxCandies for c in candies]
return ans

go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码func kidsWithCandies(candies []int, extraCandies int) []bool {
// 查找最多有多少糖果
max := 0
for _, c := range candies {
if c > max {
max = c
}
}

// 假设额外的糖果都给他
ans := make([]bool, len(candies))
for i, c := range candies {
ans[i] = c+extraCandies >= max
}

return ans
}

rust

1
2
3
4
5
6
7
8
9
10
rust复制代码impl Solution {
pub fn kids_with_candies(candies: Vec<i32>, extra_candies: i32) -> Vec<bool> {
// 查找最多有多少糖果
let max = candies.iter().max().unwrap();
// 假设额外的糖果都给他
candies.iter().map(|c| {
c + extra_candies >= *max
}).collect()
}
}

在这里插入图片描述


原题传送门:https://leetcode-cn.com/problems/kids-with-the-greatest-number-of-candies/


欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章


本文转载自: 掘金

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

听说你想看CAS原理 什么是CAS CAS原理剖析 CAS的

发表于 2021-10-26

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

什么是CAS

CAS又叫比较并交换,是一种无锁算法,日常开发中,基本不会直接用到CAS,都是通过一些JDK封装好的并发工具类来使用的,在JUC包下。

CAS包含三个值,内存地址(V),预期值(A),新值(B)。先比较内存地址的值和预期的值是否相等,如果相等,就将新值赋在内存地址上,否则,不做任何处理。步骤如下:

1.获得字段的期望值(oldValue)。

2.计算出需要替换的新值(newValue)。

3.通过CAS将新值(newValue)放在字段的内存地址上,如果CAS失败则重复第1步到第2步,一直到CAS成功,这种重复也就是CAS自旋。
自旋.PNG

当CAS进行内存地址的值与预期值比较时,如果相等,则证明内存地址的值没有被修改,可以替换成新值,然后继续往下运行;如果不相等,说明明内存地址的值已经被修改,放弃替换操作,然后重新自旋。当并发修改的线程少,冲突出现的机会少时,自旋的次数也会很少,CAS性能会很高;当并发修改的线程多,冲突出现的机会高时,自旋的次数也会很多,CAS性能会大大降低。所以,提升CAS无锁编程的效率,关键在于减少冲突的机会。

CAS原理剖析

以AtomicInteger原子整型类为例,看一下CAS底层实现机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

//这里主要就是获取AtomicInteger类value这个这个字段在地址的偏
//移量,也就是地址,这个value字段是使用的volatile 进行修饰的,保证了字段的可见性
static {
try {
//获取value值的内存偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
//AtomicInteger初始化就是对value进行赋值
public AtomicInteger(int initialValue) {
value = initialValue;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
arduino复制代码//实际对数据操作的是unsafe的类的getAndAddInt方法
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}

// 提供自增易用的方法,返回增加1后的值
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// 额外提供的compareAndSet方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

// Unsafe类的提供的方法
public final int getAndAddInt (Object o,long offset, int delta){
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}

AtomicInteger 内部方法都是基于Unsafe类实现的,Unsafe类是个跟底层硬件CPU指令通讯的复制工具类。
再看看unsafe.getAndAddInt方法具体内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码//CAS自旋,通过getIntVolatile方法通过内存偏移量获取对象最新的值,再调用cas方法,如果失败了就不断的重
//试获取对象新值然后CAS,直到成功
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//先去内存中获取内存地址所指向的值,这个将这个内存值赋值给var5,这个方法是native方法
//var5是预期值
var5 = this.getIntVolatile(var1, var2);
//在修改前先比较一次内存的值还是否是预期值var5,将偏移量var2、对应的unsafe对象var1
//和var5带进compareAndSwapInt去获取此时内存中偏移量所指向的数值然后
//和预期值var5进行比较,如果是相等的就将偏移量var2指向的主内存地址中的值修改为
//var5 + var4,失败就进行重试,每次重试都会去内存中重新获取值赋值给var5,然后修改时再比较一下
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}

再看看如何获得valueOffset的:

1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码// Unsafe实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
try {
// 获得value在AtomicInteger中的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 实际变量的值
private volatile int value;

value实际的变量,是由volatile关键字修饰的,为了保证在多线程下的内存可见性。

CAS的问题

ABA问题

CAS操作是先比较A的预期值和内存地址中的值是否相同,如果相同就认为此时没有其他线程修改A值。但是,此时假如一个线程读取到A值,此时有另外一个线程将A值改成了B,然后又将B改回了A,这时比较A和预期值是相同的,就认为A值没有被改变过。为了解决ABA的问题,可以使用版本号,每次修改变量,都在这个变量的版本号上加1,这样,刚刚A->B->A,虽然A的值没变,但是它的版本号已经变了,再判断版本号就会发现此时的A已经被别人偷偷改过了。

解决方法:AtomicReference原子引用。

性能问题

如果自旋长时间不成功,会给CPU带来非常大的执行开销。

1
2
3
4
5
6
7
sql复制代码public final int getAndSet(int newValue) {
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}

可以看到源码中的自旋就是当CAS成功时,才会return。因此CAS带来的性能问题也是需要考虑的。自旋也是CAS的特点,自旋算是一种非阻塞算法,相对于其他阻塞算法而已,非阻塞是不需要cpu切换时间片保存上下文的,节省了大量性能消耗。CAS相对于同步锁的优点:如果在并发量不是很高时CAS机制会提高效率,但是在竞争激烈并发量大的情况下效率是非常低,因为自旋时间过长,失败次数过多造成重试次数过多。

只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

ABA问题解决办法

加版本号

每次修改变量,都在这个变量的版本号上加1,这样,刚刚A->B->A,虽然A的值没变,但是它的版本号已经变了,再判断版本号就会发现此时的A已经被改过了。参考乐观锁的版本号,这种做法可以给数据带上了一种实效性的检验。

AtomicStampReference的compareAndSet方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳标志的值更新为给定的更新值。

使用AtomicMarkableReference

AtomicMarkableReference不关心修改过几次,仅仅关心是否修改过。其标记属性mark是boolean类型,而不是数字类型,标记属性mark仅记录值是否有过修改。
AtomicMarkableReference适用只要知道对象是否有被修改过,而不适用于对象被反复修改的场景。

CAS的使用场景

CAS在JUC包中的原子类、AQS以及CurrentHashMap等重要并发容器类的实现上都有应用。再看一下AQS的例子:

1
2
3
arduino复制代码protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

对state变量进行的CAS操作,很多同步类都是通过这个变量来实现线程安全的,所以在AQS中,首先要保证对state的赋值是线程安全的。

在java.util.concurrent.atomic包的原子类如AtomicXXX 中,都使用了CAS保障对数字成员进行操作的原子性。
JUC的大多数类(包括显示锁、并发容器)都基于AQS和AtomicXXX实现,而AQS通过CAS保障其内部双向队列头部、尾部操作的原子性。

抽奖说明

1.本活动由掘金官方支持 详情可见juejin.cn/post/701221…

2.通过评论和文章有关的内容即可参加,要和文章内容有关哦!

3.本月的文章都会参与抽奖活动,欢迎大家多多互动!

4.除掘金官方抽奖外本人也将送出周边礼物(马克杯一个和掘金徽章若干,马克杯将送给走心评论,徽章随机抽取,数量视评论人数增加)。

本文转载自: 掘金

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

区块链交易隐私如何保证?华为零知识证明技术实战解析

发表于 2021-10-26

​​摘要:本文通过介绍华为如何在同态加密及零知识证明框架的集成介绍来介绍了一些对金融领域交易隐私保护的思路,通过代码结和应用场景描述了zksnark如何集成到现有联盟链体系保护交易隐私。

本文分享自华为云社区《区块链交易隐私如何保证?华为零知识证明技术实战解析》,作者:麦冬爸 。

什么是零知识证明?

证明者在不泄露任何有效知识的情况下,验证者可以验证某个论断是正确的。图1给出一个有趣的例子,Alice把自己的签名的信封放到一个保险箱中,Bob说他知道这个保险箱的密码,Alice让Bob证明给她看。Bob打开保险箱,把信封拿出来给Alice。Alice验证信封上的签名,确认了Bob确实知道这个密码。这个例子就是Bob没有告诉Alice密码却证明自己知道密码的的过程很好的解释了零知识证明的概念。基于非对称加密和数字签名的证书认证过程,其实也是一个零知识证明的过程,验证者并不需要知晓CA证书,就可以验证对方是由CA签发的下一级证书。零知识证明技术不管应用于金融还是其他领域,都可以对隐私保护,性能提升,或者安全性等场景带来很多帮助。下面,主要从隐私维度来分享华为零知识证明相关技术。

图1 零知识证明

图1 零知识证明

零知识证明应用于同态加密保护交易隐私,使能金融业务

目前金融转账交易场景中对于隐私保护已经越来越重视,隐私也成为区块链急需解决的一个重要问题。那基于如下问题, A向B转账10元,需要区块链节点记账,但是不想让区块链节点知道交易金额以及最新余额,也是金融场景中一个非常常见的问题。

图2 同态加密

图2 同态加密

基于这种场景如何解决区块链技术应用于金融的隐私和可用性?华为目前引入同态加密(解决隐私问题)。同态加密(英語:Homomorphic encryption)是一种加密形式,它允许人们对密文进行特定形式的代数运算得到仍然是加密的结果,将其解密所得到的结果与对明文进行同样的运算结果一样。换言之,这项技术令人们可以在加密的数据中进行诸如检索、比较等操作,得出正确的结果,而在整个处理过程中无需对数据进行解密。在此基础上创新式提出了同态加密范围证明(一种针对数字的零知识证明技术,在不泄露具体数字值的情况下,获得数字的范围,从而验证数字所代表的交易的有效性)。

基于集成到区块链系统中的同态加密库以及修改同态加密库实现的零知识证明能力实现了隐私转账的能力,一个密文和另一个密文相加或相乘实现转账中的密文交易,零知识证明在整个的计算过程中不暴露任一方的信息证明对方可以完成转账这一流程,在不泄露具体数字的情况下得到数字的范围。从而验证数字所代表交易的有效性。

使用同态加密库的app端sample代码示例

下面我们看一下零知识证明在代码中是如何应用的,Demo代码使用地址:support.huaweicloud.com/devg-bcs/bc…。下面讲解的代码都可以在上面的地址中下载全量代码查看。

图3 同态加密链代码1

图3 同态加密链代码1

图4 同态加密链代码2

图4 同态加密链代码2

图3是同态加密的链代码,首先定义好一个transaction的结构,在进行初始化,基于Query方法可以根据B的地址调用链码获取B的公钥 ,第二个红框获取A的当前加密余额,PrepareTxInfo方法构建A向B的转账信息,最后通过invoke调用完成A到B的转账的过程。在图4 transfer链代码方法中,首先通过getstate获取A和B两个账户的当前余额,然后最重要的一步,要去验证他的余额,所以说我们这个方法validatetxinfo,基于范围/等式证明验证交易数据的合规性,基于同态加密算法计算交易后的账户余额,最后更新交易后A账户和B账户的余额。同态加密的这一步中,应用了零知识证明的相关的这个技术和能力来实现了这个同态加密更加的高效和安全。

基于zksnark的零知识证明技术

交互式证明和非交互式证明

图5 交互式证明

图5 交互式证明

零知识证明又分为交互式证明和非交互式证明,有两个有趣的例子很好的解释了这个概念。如上图5所示,男子向女子声称有CD处的钥匙,女子不相信说“你拿出来给我看啊”,男子想“你让我拿我就拿多没面子啊”,男子说”这样吧,按下面步骤玩个游戏”

1.女子站在A点

2.男子从B点走到C点或者D点

3.男子从B点消失后,女子从A点走到B点

4.女子喊话“从左边出来”,或者“从右边出来”

5.男子按照女子的要求从对应一侧走出

女子说“你肯定作弊,刚才我喊左边出来,你刚好就是从左边进去的”,

男子说:“你回到A点,我们再来一遍”

如果每次都成功,说明B确实有CD处的钥匙,该证明是需要A,B不停的交互。

非交互式零知识(NizK)证明方案由算法设置、证明和验证定义,具体来说,我们有params=Setup(),其中输入是安全参数,输出是ZKP算法系统的参数。证明语法由证明=证明(x,w)给出。该算法接收某些NP语言L的实例x和见证w作为输入,并输出零知识证明。验证算法接收证明作为输入,并输出位b,如果验证者接受证明,则该位等于1。通俗一点就比如说我有一个秘密,我不想告诉别人,但是我又得让别人相信。我是知道这个秘密的,类似于这种,但是我们为什么需要有这种非交互式呢?因为交互式证明的其实只对原始的验证者有效,其他的任何人都不能够信任这个证明。这种场景下呢,就会导致这个验证者可以和这个证明人串通,证明人可以伪造证明。验证者也可以用这种方式做一些伪造。因此,验证者必须保存一些数据,直到相关的证明被验证完毕。这样就会造成一些秘密参数泄露的这种风险。这种交互式证明也有它的用处,就比如说一个证明人只想让一个特定的验证者来去验证,但是这个证证明人和验证者必须保持在线,并且去对每一个验证者执行同样的计算。

什么是zk-snark?

zk-SNARK 是 Zero-knowledge succinctnon-interactive arguments of knowledge 的缩写,他的意思是: zero knowledge:零知识,即在证明的过程中不透露任何隐私数据: succinct:简洁的,主要是指验证过程不涉及大量数据传输以及验证算法简单; non-interactive:无交互。证明者与验证者之间不需要交互即可实现证 明,交互的零知识证明要求每个验证者都要向证明者发送数据来完成证明,而无交互的零知识证明,证明者只需要计算一次产生一个 proof,所有的验 证者都可以验证这个 proof。 zk-SNARK 是证明某个声明是真却不泄露关于该声明的隐私信息的一 个很有创新性的算法,他可以证明某人知道某个秘密却不会泄露关于这个秘密的任何信息。这个算法可以解决什么问题呢?

它是对所有零知识证明问题的通用解决方法,由加密数字货币zcash首次使用并开源。zk-SNARK的优点:

1.通用库,可以解很多零知识证明问题

2.验证证明性能较高(300tps)

zk-SNARK的不足:

1.底层模型不容易理解,用户需要根据具体的零知识证明问题,在上层构建自己的业务模型,这块开发的工作量较大。

2.生成每笔交易时延较长(57s)

应用场景

ZKP的应用场景包括匿名可验证投票、数字资产安全交换、安全远程生物识别认证和安全拍卖,具体如下。

匿名可核查投票:投票是保障一个国家或控股公司民主的重要组成部分。然而,选民的隐私可能在投票过程中被泄露。此外,投票结果很难得到安全的核实。ZKP是实施匿名可核查投票的一种可用方法。根据ZKP的使用,符合条件的选民可以在不泄露身份的情况下投票表决显示他们的权利。此外,ZKP允许符合条件的选民要求提供可核查的证据,证明他们的选票包含在负责报告投票结果的机构的最终计票中。

数字资产的安全交换:数字资产是二进制数据的集合,它们是唯一可识别和有价值的。如果两个用户希望交换其数字资产,则用户的隐私,包括身份和交换数字资产的内容,可能会在交换过程中泄露。根据ZKP的使用,数字资产可以在不泄露用户隐私的情况下交换。此外,ZKP生成了可验证的证据,其中包含数字资产交换的过程。

安全远程生物识别身份验证:远程生物识别身份验证是一种可用于通过使用指纹、面部图像、虹膜或血管模式等生物识别模式识别用户访问权限的方法。但是,在实施远程生物识别认证时,用户的生物识别模式可能会泄露给不受信任的第三方。使用ZKP可以解决这个问题。此外,ZKP生成还提供了可核查的证据,其中包括识别用户访问权限的过程。

安全拍卖:政府拍卖是政府从多个供应商中选择最低出价的拍卖,这些供应商以竞争性方式销售其商品和服务。本次拍卖包括两个阶段。在第一阶段,多个供应商投标,但公众不知道。在第二阶段,这些投标是开放的。政府选择中标供应商,后者出价最低。然而,中标供应商的选择可能会泄露其他中标供应商的投标和身份。ZKP可以解决这个问题。ZKP为每个输标供应商生成可核查的证据。该证明证实输标供应商的投标与中标供应商的投标之间的差额是正的。

zk-snark应用于区块链的挑战及实现

零知识证明是指一方(证明者)向另 一方(验证者)证明一个陈述是正确的,而无需透露除该陈述正确以外的任何信 息,适用于解决任 何NP问题。而区块链 恰好可以抽象成多方验证交易是否有效(NP问题)的平台,因此,两者是天然相适 应的。将零知识证明应用到区块链中 需要考虑的技 术挑战分为两大类:一类 是适用于隐私保护的区块链架构设计方案,包括隐秘交易所花资产存在性证明、匿 名资产双花问题、匿名资产花费与转移、隐秘交易不可区分等技术挑战;另一类是零 知识证明技术 本身带来的挑战,包括 参数 初始化阶段、算法性能以及安 全问题等技术挑战。

华为集成了zksnark架构到区块链系统中来解决上面的挑战。我们知道有多种方法可以为区块链启用zkSNark。这些都降低了配对函数和椭圆曲线操作的实际成本。

  1. 提高合约虚拟机的性能

相较第二种更难实现。可以在合约虚拟机中添加功能和限制,这将允许更好的实时编译和解释,而无需在现有实现中进行太多必要的更改。下面的转账场景就是基于此种方案的实现。

  1. 仅提高某些配对函数和椭圆曲线乘法的在合约虚拟机的性能

通过强制所有区块链客户端实现特定的配对函数和在特定椭圆曲线上的乘法作为所谓的预编译契约来实现。好处是,这可能更容易和更快地实现。另一方面,缺点是我们固定在一定的配对函数和一定的椭圆曲线上。区块链的任何新客户端都必须重新实施这些预编译的合同。此外,如果有人找到更好的zkSNark、更好的配对函数或更好的椭圆曲线,或者如果在椭圆曲线、配对函数或zkSNark中发现缺陷,必须添加新的预编译合同。

转账应用

图6 转账初始化

图6 转账初始化

图6包含了对这个余额初始化的一个过程,及生成交易也就是真正转账的过程,下一步就是验证,证明,完成验证,最后一步,才是生成交易,也就是收款的一个过程。拿初始化举个例子,比如说爱丽丝初始化了一个100块钱的一个余额,然后鲍勃十块钱。转账的过程如下,爱丽丝转20块钱给鲍勃,就会生成生成一对Spending key / Paying Key ,相当于临时交易的一个账户,Paying Key给对方,SpendingKey留给自己,用于证明交易链上的交易是谁的。

然后再基于这个生成相应的这个交易和相关的证明。完成交易生成这一步。下一步进行转账的验证证明的这个过程,验证逻辑如下, 验证逻辑:Nullifier NF.axxxxx1 和NF.axxxxx2 是否在Nullifiers 列表中,也就是说,是否有被花过;验证NF.axxxxx1 和NF.axxxxx2 是格式是否合法的花费凭据,且对应的commitment在链上(Proof + Merkle tree root),这里有需要验证Merkle tree root在 是有效的;验证input == output 金额守恒,即:100 + 0 = 80+0+20;数字范围满足要求:100-20 >0 && 20 > 0,把这个过程都验证完了以后呢,最后一步就是完成验证。完成验证的话,他其实还会做一些这个类似于交易内容的隐藏,身份隐藏,交易行为的隐藏,来保护整个的这个转账交易的过程的这个安全性。包括做一些类似于混淆电路的能力。有混淆的交易内容,且加密,验证者并不知道使用链上是哪个Commitment作为输入,只知道没有被花过,且在链上。身份隐藏让其无法确定接收方是谁,交易行为隐藏让其无法确定这个交易是发送还是接收。做一些安全性的保证之后呢,然后来完成整个的验证的过程。最后,生成交易,然后收款,整个转账过程就结束了。基于零知识证明的一个简单的一个能力,包括一个基础的转账,被华为集成在整个零知识证明使用接口中。集成的整个零知识证明架构也能用来开发一些零知识证明基础应用,做一些简单的解决方案。

总结

交易隐私保护这块的技术应该是比较多的,零知识证明技术并不一定是一个最好的选择,在安全领域中还有很多诸如同态,秘密分享,不经意传输,或者基于TEE硬件的一些隐私保护能力,可以去做一些隐私保护。但是零知识证明其优点也是很显著,未来区块链的隐私保护仍然任重而道远,如何实现快速高效、可信的零知识证明算法以及如何实现能够抵抗量子计算的零知识证明算法,都是需要进一步解决的问题。基于线上我们提供的一些基本的能力,要是大家感兴趣,可以到之前的网址下载相应的代码示例。

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

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

Elasticsearch 中为什么选择倒排索引而不选择 B

发表于 2021-10-26

前言

索引可能大家都不陌生,在用关系型数据库时,一些频繁用作查询条件的字段我们都会去建立索引来提升查询效率。在关系型数据库中,我们一般都采用 B 树索引进行存储,所以 B 树索引也是我们接触比较多的一种索引数据结构,然而在 es 中,进行全文搜索的时候却并没有选择使用 B 树 索引,而是采用的倒排索引。本文就让我们来看看 es 中的倒排索引是如何存储和检索的吧。

为什么全文索引不使用 B+ 树进行存储

关系型数据库,如 MySQL,其选择的是 B+ 树索引,如下图就是一颗简单的的 B+ 树示例:

1635213134(1).png

上图中蓝色的表示索引值,白色的表示指针,最底层叶子节点除了存储索引值还会存储整条数据(InnoDB 引擎),而根节点和枝节点不会存储数据,B+ 树之所以这么设计就是为了使得根节点和枝节点能够存储更多的节点,因为搜索的时候从根节点开始搜索,每查询一个节点就是一次 IO 操作,所以一个节点能存储更多的索引值能减少磁盘 IO 次数。

那么到这里我们就可以思考这个问题了,假如索引值本身就很大,那么 B+ 树是不是性能会急剧下降呢?答案是肯定的,因为当索引值很大的话,一个节点能存储的数据会大大减少(一个节点默认是 16kb 大小),B+ 树就会变得更深,每次查询数据所需要的 IO 次数也会更多。而且全文索引就是需要支持对大文本进行索引的,从空间上来说 B+ 树不适合作为全文索引,同时 B+ 树因为每次搜索都是从根节点开始往下搜索,所以会遵循最左匹配原则,而我们使用全文搜索时,往往不会遵循最左匹配原则,所以可能会导致索引失效。

总结起来 B+ 树不适合作为全文搜索索引主要有以下两个原因:

  • 全文索引的文本字段通常会比较长,索引值本身会占用较大空间,从而会加大 B+ 树的深度,影响查询效率。
  • 全文索引往往需要全文搜索,不遵循最左匹配原则,使用 B+ 树可能导致索引失效。
    上图中蓝色的表示索引值,白色的表示指针,最底层叶子节点除了存储索引值还会存储整条数据(InnoDB 引擎),而根节点和枝节点不会存储数据,B+ 树之所以这么设计就是为了使得根节点和枝节点能够存储更多的节点,因为搜索的时候从根节点开始搜索,每查询一个节点就是一次 IO 操作,所以一个节点能存储更多的索引值能减少磁盘 IO 次数。

那么到这里我们就可以思考这个问题了,假如索引值本身就很大,那么 B+ 树是不是性能会急剧下降呢?答案是肯定的,因为当索引值很大的话,一个节点能存储的数据会大大减少(一个节点默认是 16kb 大小),B+ 树就会变得更深,每次查询数据所需要的 IO 次数也会更多。而且全文索引就是需要支持对大文本进行索引的,从空间上来说 B+ 树不适合作为全文索引,同时 B+ 树因为每次搜索都是从根节点开始往下搜索,所以会遵循最左匹配原则,而我们使用全文搜索时,往往不会遵循最左匹配原则,所以可能会导致索引失效。

总结起来 B+ 树不适合作为全文搜索索引主要有以下两个原因:

  • 全文索引的文本字段通常会比较长,索引值本身会占用较大空间,从而会加大 B+ 树的深度,影响查询效率。
  • 全文索引往往需要全文搜索,不遵循最左匹配原则,使用 B+ 树可能导致索引失效。

全文检索

在全文检索当中,我们需要对文档进行切词处理,切好之后再将切出来的词和文档进行关联,并进行索引,那么这时候我们应该如何存储关键字和文档的对应关系呢?

正排索引

可能大家都知道,在全文检索中(比如:Elasticsearch)用的是倒排索引,那么既然有倒排索引,自然就有正排索引。

正排索引又称之为前向索引(forward index)。我们以一篇文档为例,那么正排索引可以理解成他是用文档 id 作为索引关键字,同时记录了这篇文档中有哪些词(经过分词器处理),每个词出现的次数已经每个词在文档中的位置。

但是我们平常在搜索的时候,都是输入一个词然后要得到文档,所以很显然,正排索引并不适合于做这种查询,所以一般我们的全文检索用的都是倒排索引,但是倒排索引却并不适合用于聚合运算,所以其实在 es 中的聚合运算用的是正排索引。

倒排索引

倒排索引又称之为反向索引(inverted index)。和正排索引相反,倒排索引使用的是词来作为索引关键字,并同时记录了哪些文档中有这个词。

在这里我们以一个英文文档为例子,之所以选择用英文文档是因为英文分词比较简单,直接以空格进行分词即可,而中文分词相对比较复杂。

我们以 Elasticsearch 官网中下面两句话作为两位文档来分析:

1
2
java复制代码Elasticsearch is the distributed search and analytics engine at the heart of the Elastic Stack.
Elasticsearch provides near real-time search and analytics for all types of data.

根据上面两句话,假设我们可以得到下面这样的一个索引结构:

term index term dictionary Posting list TF
term 索引 elasticsearch [1,2]
term 索引 search [1,2]
term 索引 elastic [1]
term 索引 provides [2]

其中:

  • term index:顾名思议,这个是为 term(经过分词后的每个词) 建立的索引,也就是通过这个索引可以快速找到当前 term 的位置,从而找到对应的 Posting list。因为在 es 中,会为每个字段都建立索引(默认存储在内存中),所以当我们的数据量非常大的时候,就需要能快速定位到这个词对应的索引所在的内存位置,所以就单独为每个 term 建立了索引,这个索引一般可以选择哈希表或者 B+ 树进行索引存储。
  • term dictionary:记录了文档中去重后的所有词(经过分词器处理)。
  • Posting list TF:记录了含有当前词的文档以及当前词出现在文档的位置(偏移量),该项信息是一个数组,上面表格中为了简单只列举了文档 id,实际上这里会存储很多信息。

这时候假如我们搜索 Elasticsearch Elastic 这样的关键字,那么会经过以下步骤:

  1. 对输入的关键字进行分词处理,得到两个词:elasticsearch 和 elastic(经过分词器之后大写字母都会转化成小写字母)。

1635213291(1).png

  1. 然后分别用这两个词进行搜索,搜索之后,发现 elasticsearch 在两个文档中都有出现,而 elastic 只在文档一中出现。
  2. 最终的搜索结果就是文档一和文档二都返回,但是因为文档一两个词都命中了,所以相关度(分数)更高,于是文档一会排在文档二前面,这就是算分的过程。不过需要注意的是,实际的这种相关度分数算法不会这么简单,而是有专门的算法来计算,命中词多的并不一定会出现在前面。

倒排索引如何存储数据

知道了倒排索引的搜索过程,那么倒排索引的数据又是如何存储的呢?

回答这个问题之前我们先来看另一个问题,那就是建立索引的目的是什么?最直接的目的肯定是为了加快检索速度,而为了达到这个目的,那么在不考虑其他因素的情况下,必然是需要占用的空间越少越好,而为了减少占用空间,可能就需要压缩之后再进行存储,而压缩之后又涉及到解压缩,所以采用的压缩算法也需要能达到快速压缩和解压的目的。

FOR 压缩

FOR 压缩算法即 Frame Of Reference。这种算法比较简单,也有一定的局限性,因为其对存储的文档 id 有一定要求。

假设现在有一亿个文档,对应的文档 id 就是从 1 开始自增。假设现在关键字 elasticsearch 存在于 1000W 个文档中,而这 1000W 个文档恰好就是从 1 到 1000W,那么假如不采用任何压缩算法,直接进行存储需要占用多少空间?

int 类型占用了 4 个字节,而 1000W 这个数量级需要 2 的 24 次方,也就是说如果用二进制来存储,在不考虑符号位的情况下也需要 24 个 bit 才能存储,而因为 Posting list TF 是一个数组,所以为了能解析出数据,文档 id=1 的数据也需要用 24 个 bit 来进行存储,这样就会极大的浪费了空间。

为了解决这个问题,我们就需要使用 FOR 算法,FOR 算法并不直接存储文档 id,而是存储差值,像这种这么规律的文档 id,差值都是 1,而 1 转成二进制就可以只使用 1 个 bit 进行存储,这样就只需要 1000W 个 bit 的空间来进行存储就够了,相比较直接存储原始文档 id 的情况下,这种场景采用 FOR 算法大大减少了空间。

上面举的这个例子是比较理想的情况,然而实际上这种概率是比较小的,那我们再来看下面这一组文档 id:

1
java复制代码1,9,15,45,68,323,457

这个数组计算差值后得到下面这个数组:

1
java复制代码8,6,30,23,255,134

这个时候如果还是直接用普通差值的算法,虽然也能节省空间,但是却并不是最优的一种解决方案,那么这个时候有没有一种更高效的方法来进行存储呢?

我们观察下这个差值数组,发现这个数组可以进一步拆分成两组:

  • [8,6,30,23]:这一组最大值为 30,只需要 5 个比特就能进行存储。
  • [255,134]:这一组最大值为 255,需要 8 个比特就能存储。

这么拆分之后,原始数据需要用 32*7=224 个比特(原始数据直接用 int 存储),普通差值需要 8*6=48 个比特,而经过分组差值拆分之后只需要 5*4+8*2=36 个比特,进一步压缩了空间,这种优势随着数据量的增加会更加明显。

但是不管采用哪种方案都有一个问题,那就是进行差值或者拆分之后,怎么还原数据,解压的时候怎么知道差值数组内的元素占用空间大小?

所以对每一个数据,还需要一块一个字节的空间大小来存储当前数组内元素占用的比特数,所以分组并不是越细越好,假如对每一个差值元素都单独存储,那么反而会比不分组更浪费空间,反之,如果每个分组内的元素足够多,那么存储占用空间的这一个字节反带来的影响就会更小或者忽略不计。

RBM 压缩

上面例子中介绍的差值都不会大相径庭,那么假如我们差值计算之后得到的数组,其每个元素差别都很大呢?比如说下面这个文档 id 数组:

1
java复制代码1000,62101,131385,132052,191173,196658

这个数组大家可以去计算一下差值,计算之后会发现一个大一个小,两个差值之间差距很大,所以这种方式就不适合于用 FOR 压缩,所以我们就需要有另外的压缩算法来提升效率,这就是 RBM 压缩。

RBM 压缩算法即 Roaring Bitmap,是在 2016 年由 S. Chambi、D. Lemire、O. Kaser 等人在论文《Better bitmap performance with Roaring bitmaps》与《Consistently faster and smaller compressed bitmaps with Roaring》中提出来的。

RBM 压缩算法的核心思想是:将 32 位无符号整数按照高 16 位进行划分容器,即最多可能有 65536 个 container。因为 65536 实际上就是 2 的 16 次方,而一个无符号 int 类型正好是需要 32 位进行存储,划分为高低位正好两边都是 16 位,也就是最多 65536 个。

划分之后根据高 16 位去找 container(比如高 16 位计算的结果是 1 就去找 container_1,2 就去找 container_2,依次类推),找到之后如果发现容器不存在,那么就会新建一个容器,并且把低 16 位存入容器内,如果容器存在,就直接将低 16 位存入容器。

这样就会出现一个现象:那就是容器最多有 65536 个,而每个容器内的元素也恰好最多是 65536 个元素。

也就是上面的数组经过计算就会得到以下容器(container_1 没有元素):

1635213343(1).png

如果说大家觉得上面的高低 16 位不好理解,那么可以这么理解,我们把数组中的元素全部除以 65536,对其取模,每得到一个模就创建一个容器,而其余数就放入对应的模所对应的容器中。因为一个 int 类型就是 2 的 32 次方,正好是 65536 的平方。

经过运算之后得到容器,那么容器中的元素又该如何进行存储呢?可以选择直接存储,也可以选择其他更高效的存储方式。在 RBM 算法中,总共有三种容器类型,分别采用不同的方法来存储容器中的元素:

  • ArrayContainer

ArrayContainer 采用 short 数组来进行存储,因为每个容器中的元素最大值就是 65535,采用 2 个字节进行存储。这种存储方式的特点是随着元素个数的增多,所需空间会一直增大。

  • BitmapContainer

BitmapContainer 采用位图的方式进行存储,也就是固定创建一个 65536 长度的容器,容器中每个元素只用一个比特进行存储,某一个位置有元素则存储 1,没有元素则存储为 0。这种存储方式的特点是空间固定就是占用 65536 个比特,也就是大小固定为 8kb。

  • RunContainer

RunContainer 比较特殊,在特定场景下会使用,比如文档 id 从 1-100 是连续的,那么采用这种容器就可以直接存 1,99,表示 1 后面有 99 个连续的数字,再比如 1,2,3,4,5,6,10,11,12,13 可以被压缩为 1,5,10,3,表示 1 后面有 5 个连续数字,10 后面有 3 个连续数字。

至于每次存储采用什么容器,需要进行一下判定,比如 ArrayContainer,当存储的元素少于 4096 个时,他会比 BitmapContainer 占用更少空间,而当大于 4096 个元素时,采用 ArrayContainer 所需要的空间就会大于 8kb,那么采用 BitmapContainer 就会占用更少空间。

倒排索引如何存储

前面我们讲了 es 中的倒排索引采用的是什么压缩算法进行压缩,那么压缩之后的数据是如何落地到磁盘的呢?采用的是什么数据结构呢?

字典树(Tria Tree)

字典树又称之为前缀树(Prefix Tree),是一种哈希树的变种,可以用于搜索时的自动补全、拼写检查、最长前缀匹配等。

字典树有以下三个特点:

  1. 根节点不包含字符,除根节点外的其余每个节点都只包含一个字符。
  2. 从根节点到某一节点,将路径上经过的所有字符连接起来,即为该节点对应的字符串。
  3. 每个节点的所有子节点包含的字符都不相同。

下图所示就是在数据结构网站上依次输入以下单词(AFGCC、AFG、ABP、TAGCC)后生成的一颗字典树:

1635213383(1).png

上图中可以发现根节点没有字母,除了根节点之外其余节点有白色和绿色两种颜色之分,这两种颜色的节点有什么区别呢?

绿色的节点表示当前节点是一个 Final 节点,也就是说当前节点是某一个单词的结束节点,搜索的时候当发现末尾节点是一个 Final 节点则表示当前字母存在,否则表示不存在。

比如我现在搜索 ABP,从根节点往下找的时候,最后发现 P 是一个 Final 节点,那就表示当前树中存在字符串 ABP,如果搜索 AFGC,虽然也能找到这些字母,但是 C 并不是一个 Final 节点,所以字符串 AFGC 并不存在。

不过字典树存在一个问题,上图中就可以体现出来,比如第二列中的后缀 FGCC 和 第三列中的 GCC 其实最后三个字符是重复的,但是这些重复的字符串都单独存储了,并没有被复用,也就是说字典树没有解决后缀共用问题,只解决了前缀共用(这也是字典树又被称之为前缀树的原因)。当数据量达到一定级别的时候,只共享前缀不共享后缀也会带来很多空间的浪费,那么如何来解决这个问题呢?

FST

要解决上面字典树的缺陷其实思路也很简单,就是除了利用字符串的前缀,同时也将相同的后缀进行利用,这就是 FST,在了解 FST 之前,我们先了解另一个概念,那就是 FSM,即:Finite State Transducer。

FSM

FSM,即 Finite State Machine,翻译为:有限状态机。如果大家有了解过设计模式中的状态模式的话,那么应该会对状态机有一定了解。有限状态机顾明思议就是状态可以全部被列举出来,然后随着不同的操作在不同的状态之间流程。

如下图所示就是一个简易的有限状态机(假设一个人一天做的事就是下面的所有状态,那么状态之间可以切换流转,下图中的数字表示状态的转换条件):

1635213413(1).png

有限状态机主要有以下两个特点:

  1. 状态是有限的,可以被全部列举出来。
  2. 状态与状态之间可以流转。

而我们今天所需要学习的 FST,其实就是通过 FSM 演化而来。

继续回到我们上面的那颗字典树,那么假如现在我们换成 FST 来存储,会得到如下的数据结构:

1635213438(1).png

上面这幅图是怎么得到的呢?字母后的数字又代表了什么含义呢?有些节点有数字,有些是空白又有什么区别呢?这幅图又是如何区分 Final 节点呢?接下来我们就一步步来来构建一个 FST。

构建 FST

首先我们知道,既然现在讲的是存储索引,所以除了 key 之外自然得有 value,否则是没有意义的,所以上图中其实字母就代表了索引关键字,也就是 key,而后面的数字代表了存储的文档 id(最终会转换成二进制存储),然而这个 每个数字代表的 id 又可能是不完整的,这个我们下面会解释原因。

  1. 首先我们收到第一个存储索引的的键值对 AFGCC/5,得到如下图:

1635213470(1).png

上图中红色代表开始节点,深灰色代表结束节点,加粗的线条代表其后面的节点是一个 Final 节点。这里有一个问题,那就是 5 为什么要存储在第一条线(没有存储数字的线上实际上是一个 null 值),实际上我存储在后面的任意一条线都可以,因为最终搜索的时候会把整条线路上所有的数字加起来得到最终的 value,这也就是上面我为什么说每一条线上的 value 可能是不完整的,因为一个 value 可能会被拆成好几个数字相加,并且存储在不同的线上。

首先这个 5 为什么要存储在第一段其实是为了提高复用率,因为越往前复用的机会可能就会越大。

  1. 继续存储第二个索引键值对 AFG/10,这时候得到下图:

1635213495(1).png

这时候我们发现,G 后面的节点存储了一个 5,其他线段上并没有存储数字,这是为什么呢?因为 10=5+5,而前面第一段已经存储了一个 5,后面一个 5 存储在任何一段线上都会影响到我们的第一个键值对 AFGCC/5,所以这时候就只能把他存储在当前索引 key 所对应的 Final 节点上(源码中有一个属性 output),因为搜索的时候,如果路过不属于自己的 Final 节点上的 value,是不会相加的,所以当我们搜索第一个索引值 AFGCC 的时候,是不会把 G 后面的 Final 节点中的 value 取出来相加的。

  1. 接下来继续存储第三个索引键值对 ABP/2,这时候得到下图:

1635213517(1).png

这时候因为 ABP 字符串和前面共用了 A,而 A 对应的 value 是 5,已经比 2 大了,所以只要共用 A,那么是无论如何也无法存储成功的,所以就只能把第一个节点 5 拆成 2+3,原先 A 的位置存储 2,那么后面的 3 遵循前面的原则,越靠前存储复用的概率越大,所以存在第二段线也就是字符 F 对应的位置,这时候就都满足条件了。

  1. 最后我们来存储最后一个索引键值对 TAGCC/6,最终得到如下图:

1635213541(1).png

这时候因为 GCC 这个后缀和前面是共用的,而恰好 GCC 之后的线上都没有存储 value,所以直接把这个 6 存储在第一段线即可,注意,如果这里再次发生冲突,那么就需要再次重新分配每一段 value,到这里我们就得到和上图中网站内生成的一样的 FST 了。

总结

本文主要讲解了在 Elasticsearch 中是如何利用倒排索引来进行数据检索的,并讲述了倒排索引中的 FOR 和 RBM 两种压缩算法的原理以及使用场景,最后对比了字典树(前缀树)和 FST 两种数据结构存储的区别,并最终得出了为什么 es 中选择 FST 而不是选择字典树来进行存储索引数据的原因。

本文转载自: 掘金

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

基于Apache Zookeeper手写实现动态配置中心(纯

发表于 2021-10-26

相信大家都知道,每个项目中会有一些配置信息放在一个独立的properties文件中,比如application.properties。这个文件中会放一些常量的配置,比如数据库连接信息、线程池大小、限流参数。

在传统的开发模式下,这种方式很方便,一方面能够对配置进行统一管理,另一方面,我们在维护的时候很方便。

但是随着业务的发展以及架构的升级,在微服务架构中,服务的数量以及每个服务涉及到的配置会越来越多,并且对于配置管理的需求越来越高,比如要求实时性、独立性。

另外,在微服务架构下,会涉及到不同的环境下的配置管理、灰度发布、动态限流、动态降级等需求,包括对于配置内容的安全与权限,所以传统的配置维护方式很难达到需求。

因此,就产生了分布式配置中心。

  • 传统的配置方式不方便维护
  • 配置内容的安全和访问权限,在传统的配置方式中很难实现
  • 更新配置内容时,需要重启

配置中心的工作流程

image-20200709192446173

图11-1
Spring Boot的外部化配置
=================

在本次课程中,我们会Zookeeper集成到Spring Boot的外部化配置中,让用户无感知的使用配置中心上的数据作为数据源,所以我们需要先了解Spring Boot中的外部化配置。

Spring Boot的外部化配置是基于Environment来实现的,它表示Spring Boot应用运行时的环境信息,先来看基本使用

Environment的使用

  • 在spring boot应用中,修改aplication.properties配置
1
properties复制代码key=value
  • 创建一个Controller进行测试
1
2
3
4
5
6
7
8
9
10
11
java复制代码@RestController
public class EnvironementController {

@Autowired
Environment environment;

@GetMapping("/env")
public String env(){
return environment.getProperty("key");
}
}

@Value注解使用

在properties文件中定义的属性,除了可以通过environment的getProperty方法获取之外,spring还提供了@Value注解,

1
2
3
4
5
6
7
8
9
10
11
java复制代码@RestController
public class EnvironementController {

@Value("${env}")
private String env;

@GetMapping("/env")
public String env(){
return env;
}
}

spring容器在加载一个bean时,当发现这个Bean中有@Value注解时,那么它可以从Environment中将属性值进行注入,如果Environment中没有这个属性,则会报错。

Environment设计猜想

Spring Boot的外部化配置,不仅仅只是appliation.properties,包括命令行参数、系统属性、操作系统环境变量等,都可以作为Environment的数据来源。

  • @Value(“${java.version}”) 获取System.getProperties , 获取系统属性
  • 配置command的jvm参数, -Denvtest=command ,然后通过@Value("${envtest}")

image-20210818164156459

图11-2

  • 第一部分是属性定义,这个属性定义可以来自于很多地方,比如application.properties、或者系统环境变量等。
  • 然后根据约定的方式去指定路径或者指定范围去加载这些配置,保存到内存中。
  • 最后,我们可以根据指定的key从缓存中去查找这个值。

扩展Environment

我们可以自己扩展Environment中的数据源,代码如下;

其中,EnvironmentPostProcessor:它可以在spring上下文构建之前可以设置一些系统配置。

CusEnvironmentPostProcessor

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 CusEnvironmentPostProcessor implements EnvironmentPostProcessor {
private final Properties properties=new Properties();
private String propertiesFile="custom.properties";

@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Resource resource=new ClassPathResource(propertiesFile);
environment.getPropertySources().addLast(loadProperties(resource));
}

private PropertySource<?> loadProperties(Resource resource){
if(!resource.exists()){
throw new IllegalArgumentException("file:{"+resource+"} not exist");
}
try {
properties.load(resource.getInputStream());
return new PropertiesPropertySource(resource.getFilename(),properties);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

custom.properties

在classpath目录下创建custom.properties文件

1
2
properties复制代码name=mic
age=18

spring.factories

在META-INF目录下创建spring.factories文件,因为EnvironmentPostProcessor的扩展实现是基于SPI机制完成的。

1
2
properties复制代码org.springframework.boot.env.EnvironmentPostProcessor=\
com.example.springbootzookeeper.CusEnvironmentPostProcessor

TestController

创建测试类,演示自定义配置加载的功能。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@RestController
public class TestController {

@Value("${name}")
public String val;

@GetMapping("/")
public String say(){
return val;
}
}

总结

通过上面的例子我们发现,在Environment中,我们可以通过指定PropertySources来增加Environment外部化配置信息,使得在Spring Boot运行期间自由访问到这些配置。

那么我们要实现动态配置中心,无非就是要在启动的时候,从远程服务器上获取到数据保存到PropertySource中,并且添加到Environment。

下面我们就开始来实现这个过程。

Zookeeper实现配置中心

在本小节中,主要基于Spring的Environment扩展实现自己的动态配置中心,代码结构如图11-3所示。

image-20210805232800966

图11-3
自定义配置中心的相关说明


在本次案例中,我们并没有完全使用EnvironmentPostProcessor这个扩展点,而是基于SpringFactoriesLoader自定义了一个扩展点,主要目的是让大家知道EnvironmentPostProcessor扩展点的工作原理,以及我们以后自己也可以定义扩展点。

代码实现

以下是所有代码的实现过程,按照下面这个步骤去开发即可完成动态配置中心。

ZookeeperApplicationContextInitializer

ApplicationContextInitializer扩展,它是在ConfigurableApplicationContext通过调用refresh函数来初始化Spring容器之前会进行回调的一个扩展方法,我们可以在这个扩展中实现Environment的扩展。

所以这个类的主要作用就是在ApplicationContext完成refresh之前,扩展Environment,增加外部化配置注入。

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复制代码public class ZookeeperApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>{
//PropertySourceLocator接口支持扩展自定义配置加载到spring Environment中。
private final List<PropertySourceLocator> propertySourceLocators;

public ZookeeperApplicationContextInitializer(){
//基于SPI机制加载所有的外部化属性扩展点
ClassLoader classLoader=ClassUtils.getDefaultClassLoader();
//这部分的代码是SPI机制
propertySourceLocators=new ArrayList<>(SpringFactoriesLoader.loadFactories(PropertySourceLocator.class,classLoader));
}
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
//获取运行的环境上下文
ConfigurableEnvironment environment=applicationContext.getEnvironment();
//MutablePropertySources它包含了一个CopyOnWriteArrayList集合,用来包含多个PropertySource。
MutablePropertySources mutablePropertySources = environment.getPropertySources();
for (PropertySourceLocator locator : this.propertySourceLocators) {
//回调所有实现PropertySourceLocator接口实例的locate方法,收集所有扩展属性配置保存到Environment中
Collection<PropertySource<?>> source = locator.locateCollection(environment,applicationContext);
if (source == null || source.size() == 0) {
continue;
}
//把PropertySource属性源添加到environment中。
for (PropertySource<?> p : source) {
//addFirst或者Last决定了配置的优先级
mutablePropertySources.addFirst(p);
}
}
}
}

创建classpath:/META-INF/spring.factories

1
2
properties复制代码org.springframework.context.ApplicationContextInitializer=\
com.gupaoedu.example.zookeepercuratordemo.config.ZookeeperApplicationContextInitializer

PropertySourceLocator

PropertySourceLocator接口支持扩展自定义配置加载到spring Environment中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public interface PropertySourceLocator {

PropertySource<?> locate(Environment environment,ConfigurableApplicationContext applicationContext);
//Environment表示环境变量信息
//applicationContext表示应用上下文
default Collection<PropertySource<?>> locateCollection(Environment environment, ConfigurableApplicationContext applicationContext) {
return locateCollection(this, environment,applicationContext);
}

static Collection<PropertySource<?>> locateCollection(PropertySourceLocator locator,
Environment environment,ConfigurableApplicationContext applicationContext) {
PropertySource<?> propertySource = locator.locate(environment,applicationContext);
if (propertySource == null) {
return Collections.emptyList();
}
return Arrays.asList(propertySource);
}
}

ZookeeperPropertySourceLocator

ZookeeperPropertySourceLocator用来实现基于Zookeeper属性配置的扩展点,它会访问zookeeper获取远程服务器数据。

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
java复制代码public class ZookeeperPropertySourceLocator implements PropertySourceLocator{

private final CuratorFramework curatorFramework;

private final String DATA_NODE="/data"; //仅仅为了演示,所以写死目标数据节点

public ZookeeperPropertySourceLocator() {
curatorFramework= CuratorFrameworkFactory.builder()
.connectString("192.168.221.128:2181")
.sessionTimeoutMs(20000).connectionTimeoutMs(20000)
.retryPolicy(new ExponentialBackoffRetry(1000,3))
.namespace("config").build();
curatorFramework.start();
}

@Override
public PropertySource<?> locate(Environment environment, ConfigurableApplicationContext applicationContext) {
System.out.println("开始加载远程配置到Environment中");
CompositePropertySource composite = new CompositePropertySource("configService");
try {
Map<String,Object> dataMap=getRemoteEnvironment();
//基于Map结构的属性源
MapPropertySource mapPropertySource=new MapPropertySource("configService",dataMap);
composite.addPropertySource(mapPropertySource);
addListener(environment,applicationContext);
} catch (Exception e) {
e.printStackTrace();
}
return composite;
}

private Map<String,Object> getRemoteEnvironment() throws Exception {
String data=new String(curatorFramework.getData().forPath(DATA_NODE));
//暂时支持json格式
ObjectMapper objectMapper=new ObjectMapper();
Map<String,Object> map=objectMapper.readValue(data, Map.class);
return map;
}
//添加节点变更事件
private void addListener(Environment environment, ConfigurableApplicationContext applicationContext){
NodeDataCuratorCacheListener curatorCacheListener=new NodeDataCuratorCacheListener(environment,applicationContext);
CuratorCache curatorCache=CuratorCache.build(curatorFramework,DATA_NODE,CuratorCache.Options.SINGLE_NODE_CACHE);
CuratorCacheListener listener=CuratorCacheListener
.builder()
.forChanges(curatorCacheListener).build();
curatorCache.listenable().addListener(listener);
curatorCache.start();
}
}

配置扩展点: classpath:/META-INF/spring.factories

1
2
properties复制代码com.gupaoedu.example.zookeepercuratordemo.config.PropertySourceLocator=\
com.gupaoedu.example.zookeepercuratordemo.config.ZookeeperPropertySourceLocator

配置动态变更逻辑

NodeDataCuratorCacheListener

NodeDataCuratorCacheListener用来实现持久化订阅机制,当目标节点数据发生变更时,需要收到变更并且应用。

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
java复制代码public class NodeDataCuratorCacheListener implements CuratorCacheListenerBuilder.ChangeListener {
private Environment environment;
private ConfigurableApplicationContext applicationContext;
public NodeDataCuratorCacheListener(Environment environment, ConfigurableApplicationContext applicationContext) {
this.environment = environment;
this.applicationContext=applicationContext;
}
@Override
public void event(ChildData oldNode, ChildData node) {
System.out.println("数据发生变更");
String resultData=new String(node.getData());
ObjectMapper objectMapper=new ObjectMapper();
try {
Map<String,Object> map=objectMapper.readValue(resultData, Map.class);
ConfigurableEnvironment cfe=(ConfigurableEnvironment)environment;
MapPropertySource mapPropertySource=new MapPropertySource("configService",map);
cfe.getPropertySources().replace("configService",mapPropertySource);
//发布事件,用来更新@Value注解对应的值(事件机制可以分两步演示)
applicationContext.publishEvent(new EnvironmentChangeEvent(this));
System.out.println("数据更新完成");
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}

EnvironmentChangeEvent

定义一个环境变量变更事件。

1
2
3
4
5
6
java复制代码public class EnvironmentChangeEvent extends ApplicationEvent {

public EnvironmentChangeEvent(Object source) {
super(source);
}
}

ConfigurationPropertiesRebinder

ConfigurationPropertiesRebinder接收事件,并重新绑定@Value注解的数据,使得数据能够动态改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Component
public class ConfigurationPropertiesRebinder implements ApplicationListener<EnvironmentChangeEvent> {
private ConfigurationPropertiesBeans beans;
private Environment environment;
public ConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans,Environment environment) {
this.beans = beans;
this.environment=environment;
}

@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
rebind();
}
public void rebind(){
this.beans.getFieldMapper().forEach((k,v)->{
v.forEach(f->f.resetValue(environment));
});
}
}

ConfigurationPropertiesBeans

ConfigurationPropertiesBeans实现了BeanPostPorocessor接口,该接口我们也叫后置处理器,作用是在Bean对象在实例化和依赖注入完毕后,在显示调用初始化方法的前后添加我们自己的逻辑。注意是Bean实例化完毕后及依赖注入完成后触发的。

我们可以在这个后置处理器的回调方法中,扫描指定注解的bean,收集这些属性,用来触发事件变更。

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
java复制代码@Component
public class ConfigurationPropertiesBeans implements BeanPostProcessor {

private Map<String,List<FieldPair>> fieldMapper=new HashMap<>();

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
Class clz=bean.getClass();
if(clz.isAnnotationPresent(RefreshScope.class)){ //如果某个bean声明了RefreshScope注解,说明需要进行动态更新
for(Field field:clz.getDeclaredFields()){
Value value=field.getAnnotation(Value.class);
List<String> keyList=getPropertyKey(value.value(),0);
for(String key:keyList){
//使用List<FieldPair>存储的目的是,如果在多个bean中存在相同的key,则全部进行替换
fieldMapper.computeIfAbsent(key,(k)->new ArrayList()).add(new FieldPair(bean,field,value.value()));
}
}
}
return bean;
}
//获取key信息,也就是${value}中解析出value这个属性
private List<String> getPropertyKey(String value,int begin){
int start=value.indexOf("${",begin)+2;
if(start<2){
return new ArrayList<>();
}
int middle=value.indexOf(":",start);
int end=value.indexOf("}",start);
String key;
if(middle>0&&middle<end){
key=value.substring(start,middle);
}else{
key=value.substring(start,end);
}
//如果是这种用法,就需要递归,@Value("${swagger2.host:127.0.0.1:${server.port:8080}}")
List<String> keys=getPropertyKey(value,end);
keys.add(key);
return keys;
}

public Map<String, List<FieldPair>> getFieldMapper() {
return fieldMapper;
}
}

RefreshScope

定义注解来实现指定需要动态刷新类的识别。

1
2
3
4
5
6
java复制代码@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshScope {

}

FieldPair

这个类中主要通过PropertyPlaceholderHelper将字符串里的占位符内容,用我们配置的properties里的替换。

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
java复制代码public class FieldPair {
private static PropertyPlaceholderHelper propertyPlaceholderHelper=new PropertyPlaceholderHelper("${","}",":",true);
private Object bean;
private Field field;
private String value;

public FieldPair(Object bean, Field field, String value) {
this.bean = bean;
this.field = field;
this.value = value;
}

public void resetValue(Environment environment){
boolean access=field.isAccessible();
if(!access){
field.setAccessible(true);
}
//从新从environment中将占位符替换为新的值
String resetValue=propertyPlaceholderHelper.replacePlaceholders(value,((ConfigurableEnvironment) environment)::getProperty);
try {
//通过反射更新
field.set(bean,resetValue);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

访问测试ConfigController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@RefreshScope
@RestController
public class ConfigController {

@Value("${name}")
private String name;

@Value("${job}")
private String job;

@GetMapping
public String get(){
return name+":"+job;
}
}

基于自定义PropertySourceLocator扩展

由于在上述代码中,我们创建了一个PropertySourceLocator接口,并且在整个配置加载过程中,我们都是基于PropertySourceLocator扩展点来进行加载的,所以也就是意味着除了上述使用的Zookeeper作为远程配置装载以外,我们还可以通过扩展PropertySourceLocator来实现其他的扩展,具体实现如下

CustomPropertySourceLocator

创建一个MapPropertySource作为Environment的属性源。

1
2
3
4
5
6
7
8
9
10
java复制代码public class CustomPropertySourceLocator implements PropertySourceLocator{

@Override
public PropertySource<?> locate(Environment environment, ConfigurableApplicationContext applicationContext) {
Map<String, Object> source = new HashMap<>();
source.put("age","18");
MapPropertySource propertiesPropertySource = new MapPropertySource("configCenter",source);
return propertiesPropertySource;
}
}

spring.factories

由于CustomPropertySourceLocator是自定义扩展点,所以我们需要在spring.factories文件中定义它的扩展实现,修改如下

1
2
3
properties复制代码com.gupaoedu.example.zookeepercuratordemo.config.PropertySourceLocator=\
com.gupaoedu.example.zookeepercuratordemo.config.ZookeeperPropertySourceLocator,\
com.gupaoedu.example.zookeepercuratordemo.config.CustomPropertySourceLocator

ConfigController

接下来,我们通过下面的代码进行测试,从结果可以看到,我们自己定义的propertySource被加载到Environment中了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@RefreshScope
@RestController
public class ConfigController {

@Value("${name}")
private String name;

@Value("${job}")
private String job;

@Value("${age}")
private String age;

@GetMapping
public String get(){
return name+":"+job+":"+age;
}
}

本文转载自: 掘金

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

使用 Laravel Horizon 优雅的终止进程(2)

发表于 2021-10-26

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

先看我

我是第一篇:使用 Laravel Horizon 优雅的终止进程(1)

抛出问题

我们发布新版本代码时,如果优雅的终止运行中的异步任务,规避异步任务运行到一半被kill掉的情况。

解决办法

通过调研之后发现,laravel 的 horizon 扩展可以解决这个问题,使用下面的命令可以优雅的结束进程:

  1. 确保进行中进程不会被kill掉,执行结束后才允许被kill;
  2. 非进行的任务等候,不会加入到队列中;

优雅的解决了这个问题,思路就是这么的朴实无华。

1
c复制代码php artisan horizon:terminate

下面继续介绍 Horizon 进阶知识点

基础知识点可以看这篇:使用 Laravel Horizon 优雅的终止进程(1)

运行 Horizon

当在 config/horizon.php 文件中配置好队列执行进程后,就可以使用 horizon Artisan 命令启动 Horizon。

只需要一条命令语句即可启动所有配置好的队列进程:

1
复制代码php artisan horizon

使用 horizon:pause 和 horizon:continue Artisan 命令来暂停或继续执行队列任务:

1
2
3
kotlin复制代码php artisan horizon:pause

php artisan horizon:continue

使用 horizon:terminate Artisan 命令优雅的终止 Horizon 主进程。

Horizon 会把正在执行的任务处理完毕后退出:

1
c复制代码php artisan horizon:terminate

部署 Horizon

将 Horizon 部署到线上服务器时,则需要配置一个进程监控器来检测 php artisan horizon 命令,在它意外退出时自动重启。

上线新代码时则需要该进程监控器终止 Horizon 进程并以修改后的代码重启 Horizon。

下面的内容比较重要,请集中注意力:

Supervisor 配置

如果使用 Supervisor 进程监控器管理 horizon 进程,那么以下配置文件则可满足需求:

1
2
3
4
5
6
7
8
ini复制代码[program:horizon]
process_name=%(program_name)s
command=php /home/forge/app.com/artisan horizon
autostart=true
autorestart=true
user=forge
redirect_stderr=true
stdout_logfile=/home/forge/app.com/horizon.log

标签

Horizon 允许你对任务分配「标签」,包括邮件,事件广播,通知和队列的事件监听器。

优雅如Laravel,Horizon 会智能并且自动根据任务携带 Eloquent 模型给大多数任务标记标签,如下任务示例:

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
php复制代码<?php

namespace App\Jobs;

use App\Video;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

class RenderVideo implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* video 实例
*
* @var \App\Video
*/
public $video;

/**
* 创建工作实例
*
* @param \App\Video $video
* @return void
*/
public function __construct(Video $video)
{
$this->video = $video;
}

/**
* 执行任务
*
* @return void
*/
public function handle()
{
//
}
}

如果该队列任务是一个携带 id 为 1 的 App\Video 实例,那么它将自动被标记上 App\Video:1 标签。

因为 Horizon 会检查任务属性是否具有 Eloquent 模型,如果发现 Eloquent 模型,Horizon 将会智能的用该模型的类名和主键为任务标记上标签:

1
2
3
css复制代码$video = App\Video::find(1);

App\Jobs\RenderVideo::dispatch($video);

再一次发出感慨:真的优雅!

自定义标签

需要手动的为队列执行的对象定义标签,可以给这个类定义一个 tags 方法:

1
2
3
4
5
6
7
8
9
10
11
12
php复制代码class RenderVideo implements ShouldQueue
{
/**
* 获取分配给这个任务的标签
*
* @return array
*/
public function tags()
{
return ['render', 'video:'.$this->video->id];
}
}

通知

如果需要在队列等待时间过长时发起通知,可以在应用的 HorizonServiceProvider 中调用以下三种方法:

Horizon::routeMailNotificationsTo

Horizon::routeSlackNotificationsTo

Horizon::routeSmsNotificationsTo

1
2
3
4
5
6
css复制代码Horizon::routeMailNotificationsTo('example@example.com');

//这个不常用,起码我没用过。
Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');

Horizon::routeSmsNotificationsTo('138xxxxxxxx');

题外话:Slack是什么?

百度百科的解释:Slack 是聊天群组 + 大规模工具集成 + 文件整合 + 统一搜索。截至2014年底,Slack 已经整合了电子邮件、短信、Google Drives、Twitter、Trello、Asana、GitHub 等 65 种工具和服务,可以把各种碎片化的企业沟通和协作集中到一起。

配置等待时间过长的阈值

可以在 config/horizon.php 配置文件中设置等待时间过长的具体秒数。

waits 配置项可以针对每一个 链接 / 队列 配置阈值:

1
2
3
dart复制代码'waits' => [
'redis:default' => 60, //单位秒
],

Metrics

Horizon 包含一个 Metrics 仪表盘,它可以提供任务和队列等待时间和吞吐量信息。

为了填充此仪表盘,需要通过应用的任务调度器配置 Horizon 的 snapshot Artisan 命令每五分钟运行一次。

1
2
3
4
5
6
7
8
9
10
php复制代码/**
* 定义应用程序的任务调度
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('horizon:snapshot')->everyFiveMinutes();
}

总结

文章开头抛出的问题是我开发过程中没考虑到的,

初识文档也没体会到 php artisan horizon:terminate 的妙处。

随着项目的不断成长,自己的技术水平和编程思想也在随之成长。

好了,Laravel Horizon的内容就总结到这里了,真的优雅~

感谢支持

文章看到这里就点赞关注之后再走呗,感谢❤️

本文转载自: 掘金

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

常见For循环写法及优化 常见For循环写法及优化

发表于 2021-10-26

常见For循环写法及优化

小知识,大挑战!本文正在参与“ 程序员必备小知识”创作活动

本文同时参与 掘力星计划,赢取创作大礼包,挑战创作激励金

引言

我们都经常使用一些循环耗时计算的操作,特别是for循环,它是一种重复计算的操作,如果处理不好,耗时就比较大,如果处理书写得当将大大提高效率,下面总结几条for循环的常见优化方式。

首先,我们初始化一个集合 list,如下:

方法一:最常规的不加思考的写法

1
2
3
java复制代码for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
  • 优点:较常见,易于理解
  • 缺点:每次都要计算list.size()

方法二:数组长度提取出来

1
2
3
4
java复制代码int m = list.size();
for (int i = 0; i < m; i++) {
System.out.println(list.get(i));
}

优点:不必每次都计算

缺点:

  • m的作用域不够小,违反了最小作用域原则;
  • 不能在for循环中操作list的大小,比如除去或新加一个元素

方法三:数组长度提取出来

1
2
3
java复制代码for (int i = 0, n = list.size(); i < n; i++) {
System.out.println(list.get(i));
}

优点:不必每次都计算 ,变量的作用域遵循最小范围原则

缺点:

  • m的作用域不够小,违反了最小作用域原则;
  • 不能在for循环中操作list的大小,比如除去或新加一个元素

方法四:采用倒序的写法

1
2
3
java复制代码for (int i = list.size() - 1; i >= 0; i--) {
System.out.println(list.get(i));
}

优点:不必每次都计算 ,变量的作用域遵循最小范围原则

缺点:

  • 结果的顺序会反;
  • 看起来不习惯,不易读懂

适用场合:与显示结果顺序无关的地方:比如保存之前数据的校验

方法五:Iterator 遍历

1
2
3
java复制代码for (Iterator<String> it = list.iterator(); it.hasNext();) {
System.out.println(it.next());
}

优点:简洁

如果添加或移除元素会直接报java.util.ConcurrentModificationException异常

方法六:jdk1.5后的写法

1
2
3
java复制代码for (Object o : list) {
System.out.println(o);
}

优点:简洁结合泛型使用更简洁

缺点:jdk1.4向下不兼容

方法七:lamada写法

1
java复制代码list.forEach(i-> System.out.println(i));

优点:代码简洁

缺点:可读性变差

方法七:循环嵌套外小内大原则

1
2
3
4
5
java复制代码for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10000; j++) {
// 逻辑处理
}
}

原因:

微信图片_20211026093134.jpg

方法八:循环嵌套提取不需要循环的逻辑

1
2
3
4
5
6
7
8
9
10
11
java复制代码//前:
int a = 10, b = 11;
for (int i = 0; i < 10; i++) {
i = i * a * b;
}

//后:
int c = a * b;
for (int i = 0; i < 10; i++) {
i = i * c;
}

方法九:异常处理写在循环外面

反例

1
2
3
4
5
6
7
java复制代码for (int i = 0; i < 10; i++) {
try {

} catch (Exception e) {

}
}

正例

1
2
3
4
5
6
java复制代码try {
for (int i = 0; i < 10; i++) {
}
} catch (Exception e) {

}

本文转载自: 掘金

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

1…467468469…956

开发者博客

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