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

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


  • 首页

  • 归档

  • 搜索

【Sentinel系列】Sentinel Dashboard

发表于 2021-11-23

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

Nacos 作为项目的配置中心角色,但是存在一种情况是一部分操作者在Nacos控制台上动态修改流控规则并且实现流控规则同步,但是有另外一部分操作者通过Sentinel Dashboard来修改流控规则,这种情况导致Nacos上的流控规则和Sentinel Dashboard上流控规则数据不一致的问题。

Nacos作为配置的持久化数据库存储中心,不建议在Nacos上修改流控规则,因为Nacos配置流控规则不是本职工作所负责的,而是Sentinel DashBoard专门负责管理流控规则的控制台,对流控规则配置更加清晰、可视化,所以流控规则的管理集中在Sentinel Dashboard上。但是需要实现Sentinel Dashboard动态修改流控规则能同步到Nacos。

Sentinel Dashboard 整合Nacos 实现 流控规则 双向同步

Sentinel Dashboard的【流控规则】下的所有操作,都是调用Sentinel Dashboard源码中FlowControllerV1类,这个类中有流控规则本地化的增删改查操作。

在com.alibaba.csp.sentinel.dashboard.controller.v2包下有一个FlowControllerV2类,这个类提供了流控规则的增删改查操作,和V1版本不同点是它可实现数据源的规则拉取和发布。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@RestController
@RequestMapping(value="/v2/flow")
public class FlowControllerV2{
@Autowired
private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;
@Auowired
@Qualifier("flowRuleDefaultProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Auowired
@Qualifier("flowRuleDefaultPublisher")
private DynamicRulePulisher<List<FlowRuleEntity>> rulePublisher;
}

FlowControllerV2依赖以下两个重要的类

  1. DynamicRulePulisher:动态规则的发布,在Sentinel Dashboard 中修改规则同步到指定数据源。
  2. DynamicRuleProvider:动态规则的拉取,从指定数据源中获取流控规则后在Sentinel Dashboard中显示。

需要扩展DynamicRuleProvider和DynamicRulePulisher,整合Nacos实现Sentinel Dashboard流控规则双向同步。

Sentinel Dashboard 同步流控规则到Nacos

修改Sentinel Dashboard代码,具体流程步骤如下:

  1. 在github中拉取Sentinel 1.7.2 的源码
  2. 使用IDEA打开Sentinel源码
  3. 在Sentinel-dashboard模块pom.xml文件将sentinel-datasource-nacos依赖的去掉。
1
2
3
4
5
xml复制代码<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<!--<scope>test</scope>-->
</dependency>

本文转载自: 掘金

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

ElasticSearch不支持分组查询么?

发表于 2021-11-23

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

ElasticSearch分组查询非预期返回

在使用es进行组合查询的时候,遇到一个非常有意思的场景,特此记录一下

某些场景下,直接针对某个Field进行分组查询,居然无法返回结果,会给出类似Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default的提示信息,接下来看一下这个问题是个什么情况,以及如何解决

1. 数据准备

初始化一个索引,写入一些测试数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
json复制代码post second-index/_doc
{
"url": "/test",
"execute": {
"args": "id=10&age=20",
"cost": 10,
"res": "test result"
},
"response_code": 200,
"app": "yhh_demo"
}


post second-index/_doc
{
"url": "/test",
"execute": {
"args": "id=20&age=20",
"cost": 11,
"res": "test result2"
},
"response_code": 200,
"app": "yhh_demo"
}


post second-index/_doc
{
"url": "/test",
"execute": {
"args": "id=10&age=20",
"cost": 12,
"res": "test result2"
},
"response_code": 200,
"app": "yhh_demo"
}


post second-index/_doc
{
"url": "/hello",
"execute": {
"args": "tip=welcome",
"cost": 2,
"res": "welcome"
},
"response_code": 200,
"app": "yhh_demo"
}

post second-index/_doc
{
"url": "/404",
"execute": {
"args": "tip=welcome",
"cost": 2,
"res": "xxxxxxxx"
},
"response_code": 404,
"app": "yhh_demo"
}

2. 分组查询基本知识点

相当于sql中的group by,常用于聚合操作中的统计计数的场景

在es中,使用aggs来实现,语法如下

1
2
3
4
5
6
7
8
json复制代码"aggs": {
"agg-name": { // 这个agg-name 是自定义的聚合名称
"terms": { // 这个terms表示聚合的策略,根据 field进行分组
"field": "",
"size": 10
}
}
}

比如我们希望根据url统计访问计数,对应的查询可以是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码GET second-index/_search
{
"query": {
"match_all": {}
},
"size": 1,
"aggs": {
"my-agg": {
"terms": {
"field": "url",
"size": 2
}
}
}
}

直接执行上面的分组查询,结果问题来了

00.jpg

右边返回的提示信息为Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. Alternatively, set fielddata=true on [url] in order to load field data by uninverting the inverted index. Note that this can use significant memory这个异常

3. 解决方案

简单来说,上面这个问题,就是因为url这个字段为text类型,默认情况下这种类型的不走索引,不支持聚合排序,如果需要则需要设置fielddata=true,或者使用url的分词url.keyword

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码GET second-index/_search
{
"query": {
"match_all": {}
},
"size": 1,
"aggs": {
"my-agg": {
"terms": {
"field": "url.keyword",
"size": 2
}
}
}
}

01.jpg

注意

  • 虽然我们更注重的是分组后的结果,但是hits中依然会返回命中的文档,若是只想要分组后的统计结果,可以在查询条件中添加 size:0
  • 聚合操作和查询条件是可以组合的,如只查询某个url对应的计数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
json复制代码GET second-index/_search
{
"query": {
"term": {
"url.keyword": {
"value": "/test"
}
}
},
"size": 1,
"aggs": {
"my-agg": {
"terms": {
"field": "url.keyword",
"size": 2
}
}
}
}

02.jpg

上面介绍了TEXT类型的field,根据分词进行聚合操作;还有一种方式就是设置fielddata=true,操作姿势如下

1
2
3
4
5
6
7
8
9
json复制代码PUT second-index/_mapping
{
"properties": {
"url": {
"type": "text",
"fielddata": true
}
}
}

修改完毕之后,再根据url进行分组查询,就不会抛异常了

03.jpg

4. 小结

最后小结一下,当我们使用es的某个field进行分组操作时,此时需要注意

当这个field类型为text,默认的场景下是不支持分组操作的,如果非要用它进行分组查询,有两个办法

  • 使用它的索引字段,如 url.keyword
  • 在索引的filed上添加fileddata: true 配置

一灰灰的联系方式

尽信书则不如无书,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

  • 个人站点:blog.hhui.top
  • 微博地址: 小灰灰Blog
  • QQ: 一灰灰/3302797840
  • 微信公众号:一灰灰blog

本文转载自: 掘金

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

聊一聊Go服务优雅下线与重启的实现

发表于 2021-11-23

前言

最近服务高可用的重要性越来越大,高可用通常指的是通过故障转移到冗余模块,如主备切换等相应操作,用来保证系统对外提供可用性,而细化到程序下线/重启等操作,在Go里面有哪些处理方式呢?今天我们来聊聊Go程序的优雅关闭与重启,如何让程序在关闭或者重启之前对旧的连接进行处理,尽量做到无感知切换。

概念引入

进程间通讯方式

我们知道进程通信有几种常用的方式:

  • 管道
  • 信号量
  • 网络socket
  • 共享内存

今天我们先来聊一聊信号量,比如P/V信号量,常常用于进程在访问临界区时候,用于唤醒或等待临界区的其他进程,信号量本质上是操作系统发送的一个中断机制,除了P/V信号量,还有常见的场景比如我们在中断按下Ctrl+C用于通知进程退出,会发送一个interrupt信号,也叫SIGINT。

在Go里面,windows平台下的信号量语义如下:

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
go复制代码const (
// More invented values for signals
SIGHUP = Signal(0x1)
SIGINT = Signal(0x2)
SIGQUIT = Signal(0x3)
SIGILL = Signal(0x4)
SIGTRAP = Signal(0x5)
SIGABRT = Signal(0x6)
SIGBUS = Signal(0x7)
SIGFPE = Signal(0x8)
SIGKILL = Signal(0x9)
SIGSEGV = Signal(0xb)
SIGPIPE = Signal(0xd)
SIGALRM = Signal(0xe)
SIGTERM = Signal(0xf)
)

var signals = [...]string{
1: "hangup",
2: "interrupt",
3: "quit",
4: "illegal instruction",
5: "trace/breakpoint trap",
6: "aborted",
7: "bus error",
8: "floating point exception",
9: "killed",
10: "user defined signal 1",
11: "segmentation fault",
12: "user defined signal 2",
13: "broken pipe",
14: "alarm clock",
15: "terminated",
}

使用15个数字以十六进制表示,那么我们接着看,在go里面,是怎么监听系统信号量的呢?

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
go复制代码func Notify(c chan<- os.Signal, sig ...os.Signal) {
if c == nil {
panic("os/signal: Notify using nil channel")
}
// 省略部分代码...
add := func(n int) {
if n < 0 {
return
}
if !h.want(n) {
h.set(n)
if handlers.ref[n] == 0 {
enableSignal(n)

// 单例启动监听,保证程序启动之前注册相应的处理逻辑
watchSignalLoopOnce.Do(func() {
if watchSignalLoop != nil {
// 新建协程轮询监听
go watchSignalLoop()
}
})
}
handlers.ref[n]++
}
}
// 省略部分代码...
}

其中的watchSignalLoop在unix版本中,是一个轮询函数,

1
2
3
4
5
6
7
8
9
go复制代码func loop() {
for {
process(syscall.Signal(signal_recv()))
}
}

func init() {
watchSignalLoop = loop
}

至此我们知道了信号量注册和监听的大致过程了,通过注册一个与目标信号量的上下文,异步创建一个协程进行系统信号监听。


接下来我们拿interrupt来举例,监听系统的中断请求,在Go中可以用如下方式注册:

1
2
3
4
5
6
7
go复制代码// 注册返回绑定了os.Interrupt的ctx
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)

//...

// 其中stop()函数用于解绑上下文与信号量
defer stop()

通过监听os.Interrupt返回的上下文之后,如果系统调用中断,该ctx会执行终止,也就是ctx.Done(),我们可以利用这个作为我们后续处理的信号量。

优雅关闭

拿到中断信号量之后,我们来看下如何优雅退出,来看下这个函数

1
2
3
4
5
6
7
8
9
10
go复制代码// Shutdown gracefully shuts down the server without interrupting any
// active connections. Shutdown works by first closing all open
// listeners, then closing all idle connections, and then waiting
// indefinitely for connections to return to idle and then shut down.
// If the provided context expires before the shutdown is complete,
// Shutdown returns the context's error, otherwise it returns any
// error returned from closing the Server's underlying Listener(s).
func (srv *Server) Shutdown(ctx context.Context) error {
// ...
}

从注释可以看到,Shutdown()执行会先关闭打开连接,然后关闭空闲连接,接着等待已使用连接变成空闲连接,才会执行关闭。此外,如果传入的ctx上下文在执行关闭前发生过期,则Shutdown()会返回相应错误。

所以我们可以利用Shutdown(),让程序在中断处,执行最后收尾工作,另外用上下文的生命周期来把控收尾的缓冲期。

代码示例:

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
go复制代码var (
server http.Server
)

// 优雅停止demo
func main() {
// 注册返回绑定了os.Interrupt的ctx
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

server = http.Server{
Addr: ":8080",
}

// 注册路由
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Second * 3)
fmt.Fprint(w, "Hello World")
})

// 启动监听
go server.ListenAndServe()

// 触发interrupt信号
<-ctx.Done()

// 解绑上下文与信号量
stop()
log.Print("接收到SIGINT信号, 执行优雅停止, 等待收尾...")

// 最后10秒回收连接
timeoutCtx, cancelFunc := context.WithTimeout(context.Background(), 10*time.Second)
defer cancelFunc()

if err := server.Shutdown(timeoutCtx); err != nil {
fmt.Println(err)
}

log.Print("程序关闭完成.")
}
  1. 注册一个简单的路由请求,等待3秒之后返回“Hello World”
  2. 绑定系统信号量Signal.SIGNINT到上下文
  3. 通过上下文感知中断
  4. 新建10秒生存期的上下文
  5. 传入带生命周期的上下文至Shutdown()函数,用于控制收尾

输出示例

启动程序并且按下Ctrl+C,在没有请求的情况下,程序快速终止。

1
2
3
bash复制代码$ go run main.go
2021/11/06 23:58:03 接收到SIGINT信号, 执行优雅停止, 等待收尾...
2021/11/06 23:58:03 程序关闭完成.

接着我们在程序启动之后执行请求让其耗时处理

1
2
bash复制代码$ curl 127.0.0.1:8080
Hello World

并在服务端按下Ctrl+C

1
2
3
bash复制代码$ go run main.go
2021/11/06 23:58:33 接收到SIGINT信号, 执行优雅停止, 等待收尾...
2021/11/06 23:58:35 程序关闭完成.

可以看到日志输出,程序不再是立即退出,而是等待请求终止才会关闭。

而假如说我们调整请求执行逻辑耗时更长,当处理时长超过shutdown函数绑定的上下文周期,则程序会返回一个上下文超时的错误。

1
2
bash复制代码2021/11/07 00:02:46 接收到SIGINT信号, 执行优雅停止, 等待收尾...
2021/11/07 00:02:51 优雅停止错误: context deadline exceeded

抛砖引玉

以上就是优雅退出的大致实现,关于可拓展的想法:

上述主要是一个优雅下线之前的处理,生产场景下,服务下线或者不可用还有其他的具体检测措施,比如心跳包超时丢失,k8s中服务下线可以通过轮询周期监听一个本地文件/句柄来判断等,其实信号量只是我们感知程序中断的一种方式,基于服务下线,我们知道了最终可以使用Shutdown()来执行收尾。

此外,当执行收尾之后,如果遇到关联上下文已经超时的情况context deadline exceeded,业务处理层一般可以归档未处理完成的请求,放入重试队列或者以写日志的形式记录下来,归档并放在后续修复。


优雅重启

聊完优雅退出之后,后续我们再来看下程序如何优雅重启。 前阵子看到一篇信号量交互的实现,个人觉得挺有意思,所以拿出来梳理一下,文章链接会放在参考资料。

其实优雅重启核心在于我们需要有一个接盘侠,当下线的服务如果有未处理完的连接,我们需要提供一个新的服务/进程尽可能地处理,并继续持续监听新的请求,对外提供可用性,让请求端无感知。

简单来说,实现优雅重启需要解决两个问题:

  1. 如何在操作系统层面,保留原先创建的socket让新重启的进程继续监听
  2. 保证所有后续请求能够执行响应或者超时

这听起来似乎十分理想,下面我们一步一步拆解,看下是如何实现的。

核心拆解

  1. 在当前监听socket的进程下,fork一个子进程进行“接盘”
  2. 新(子)进程接替,复用原先的socket
  3. 新(子)进程通知原(父)进程停止接收请求并关闭

状态转移

我们前期不过多深入进程启动后续处理的细节,先来梳理下程序需要监听的状态,或者说程序在重启时刻需要对哪些事件做出什么响应。
其实当前服务无非两个状态,

  • 一个是首次启动
  • 另一个是版本变动启动新进程替换旧进程

状态一其实和普通的服务没有本质区别,就是启动完进行listen就好了。

来聊一聊状态二,状态二其实是由状态一延伸出来的,所以程序需要同时兼任两种状态的监听,而监听的触发事件就是上文我们在优雅停止中提到的信号量。

我画了一张大致流程图,方便后续加深理解:

graceful-state.png

前置概念

我们再来熟悉下网络Socket编程中一些概念,以便知悉如何进行连接复用。

我们知道在网络环境中,可以使用TCP四元组建立一个端到端的连接,即<src addr>, <src port>, <dest addr>, <dest port>锁定唯一连接标识。

都知道一个TCP连接断开需要经过四次挥手,其中在被断开方有个TIME_WAIT状态,用于等待被断开方关闭连接,或者是发送端缓冲区数据真正发送,这个等待时间一般是不会改变的(默认2min),也就是说在这个TIME_WAIT状态结束之前中,当前tcp元组是无法被复用的,除非设置了SO_REUSEADDR。

这里有两个关键参数,SO_REUSEADDR和SO_REUSEPORT,
首先字面上意思都是复用,具体概念如下:

参数 含义
SO_REUSEADDR 允许连接ip地址在未完全断开的情况进行复用
SO_REUSEPORT 在开启SO_REUSEADDR的前提下,允许连接端口地址进行复用

那么如果是开启复用并连接成功,在操作系统层面,假如多个文件句柄都绑定了系统的ip+port,系统会怎么处理呢,答案是负载均衡,即系统会根据请求进行分配,类似随机轮询的方式,对相同ip+port的连接进行交互。

这里可能有人会说,这样子不同客户端进程访问是否有权限越界问题呢,确实会有,所以基于安全考虑有一个约定:

To prevent “port hijacking”, there is one special limitation: All sockets that want to share the same address and port combination must belong to processes that share the same effective user ID

所有要开启复用同一地址端口的连接必须属于同一个userID,而我们的上下文中是同一个进程或者说同一用户创建处理的,所以可以复用原来的连接,从而避免恶意劫持。

程序示例

我们来看下程序如何实现

  1. 传入复用连接的配置项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码func control(network, address string, c syscall.RawConn) error {
var err error
c.Control(func(fd uintptr) {
err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
if err != nil {
return
}

err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
if err != nil {
return
}
})
return err
}
  1. 检测当前监听的tcp元组是否正在监听
1
2
3
4
5
6
7
8
9
10
11
go复制代码func listener() (net.Listener, error) {
lc := net.ListenConfig{
Control: control,
}
if l, err := lc.Listen(context.TODO(), "tcp", ":8080"); err != nil {
// 端口未使用,返回err
return nil, err
} else {
return l, nil
}
}
  1. 监听系统信号量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
go复制代码func upgradeLoop(l *net.Listener, s *http.Server) {
sig := make(chan os.Signal)
signal.Notify(sig, syscall.SIGQUIT, syscall.SIGUSR2)
for t := range sig {
switch t {
case syscall.SIGUSR2:
// 接收升级信号量
log.Println("Received SIGUSR2 upgrading binary")
// Fork子进程优雅升级
if err := spawnChild(); err != nil {
log.Println(
"Cannot perform binary upgrade, when starting process: ",
err.Error(),
)
continue
}
case syscall.SIGQUIT:
// 接收杀掉当前进程的信号量
s.Shutdown(context.Background())
os.Exit(0)
}
}
}

// fork创建子进程,并在新进程之前更新覆盖全局父进程id
func spawnChild() error {
// 获取当前启动传入可执行文件参数, 如./main
argv0, err := exec.LookPath(os.Args[0])
if err != nil {
return err
}

wd, err := os.Getwd()
if err != nil {
return err
}

files := make([]*os.File, 0)
files = append(files, os.Stdin, os.Stdout, os.Stderr)

// 存下当前进程, 这个id会在新进程启动之后kill掉
ppid := os.Getpid()
os.Setenv("APP_PPID", strconv.Itoa(ppid))

// 启动新进程
os.StartProcess(argv0, os.Args, &os.ProcAttr{
Dir: wd,
Env: os.Environ(),
Files: files,
Sys: &syscall.SysProcAttr{},
})

return nil
}
  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
go复制代码func main() {
log.Println("Started HTTP API, PID: ", os.Getpid())
var l net.Listener
// 首次启动
if fd, err := listener(); err != nil {
log.Println("Parent does not exists, starting a normal way")
l, err = net.Listen("tcp", ":8080")

if err != nil {
panic(err)
}
} else {
// 新fork出来的,当前端口已被监听
l = fd
// 发送quit给父进程
killParent()
time.Sleep(time.Second)
}

// 启动server监听
s := &http.Server{}
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
log.Printf("New request! From: %d, path: %s, method: %s: ", os.Getpid(),
r.URL, r.Method)
})
go s.Serve(l)

// 监听信号量
upgradeLoop(&l, s)
}

代码引用:

zero-downtime-application项目源码,其实核心在于新进程到旧进程的优雅迁移这个过程,只要理解了代码看起来就会清晰一点了。

这也就是为什么main函数逻辑块需要兼容两个情形,一是正常server流程,一是接收旧进程的收尾。

拓展应用

关于上面的优雅重启触发机制是用户发送信号量pkill -SIGUSR2给进程,作为一个手动升级的无缝切换。

其实基于这个功能可以进行拓展,比如监控服务加入连接探测,请求响应时间告警等,当达到某个触发机制,可以触发优雅重启,从而实现动态拉起的效果,当然后续还是需要复盘定位服务的问题在哪里,毕竟有时候重启并不能解决所有问题。

参考链接

  • Graceful Shutdowns in Golang with signal.NotifyContext
  • Zero downtime API in Golang
  • Graceful Restart in Golang
  • 基于K8S的优雅关闭
  • 关于BSD套接字参数讨论

本文转载自: 掘金

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

【golang】 panic 和 recover

发表于 2021-11-23

Go 语言中两个经常成对出现的两个关键字 — panic 和 recover。这两个关键字与上一节提到的 defer 有紧密的联系,它们都是 Go 语言中的内置函数,也提供了互补的功能。

image.png

  • panic 能够改变程序的控制流,调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer;
  • recover 可以中止 panic 造成的程序崩溃。它是一个只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥作用;

现象

  • panic 只会触发当前 Goroutine 的 defer;
  • recover 只有在 defer 中调用才会生效;
  • panic 允许在 defer 中嵌套多次调用;

跨协程失效

panic 只会触发当前 Goroutine 的延迟函数调用。

1
2
3
4
5
6
7
8
9
go复制代码func main() {
defer println("in main")
go func() {
defer println("in goroutine")
panic("")
}()

time.Sleep(1 * time.Second)
}
1
2
3
4
go复制代码$ go run main.go
in goroutine
panic:
...

运行这段代码时会发现 main 函数中的 defer 语句并没有执行,执行的只有当前 Goroutine 中的 defer。

前面曾经介绍过 defer 关键字对应的 runtime.deferproc 会将延迟调用函数与调用方所在 Goroutine 进行关联。所以当程序发生崩溃时只会调用当前 Goroutine 的延迟调用函数也是非常合理的。

image.png

多个 Goroutine 之间没有太多的关联,一个 Goroutine 在 panic 时也不应该执行其他 Goroutine 的延迟函数。

失效的崩溃恢复

在主程序中调用 recover 试图中止程序的崩溃,但是从运行的结果中能看出,下面的程序没有正常退出。

1
2
3
4
5
6
7
8
go复制代码func main() {
defer fmt.Println("in main")
if err := recover(); err != nil {
fmt.Println(err)
}

panic("unknown err")
}
1
2
3
4
5
6
7
8
less复制代码$ go run main.go
in main
panic: unknown err

goroutine 1 [running]:
main.main()
...
exit status 2

recover 只有在发生 panic 之后调用才会生效。然而在上面的控制流中,recover 是在 panic 之前调用的,并不满足生效的条件,所以需要在 defer 中使用 recover 关键字。

嵌套奔溃

Go 语言中的 panic 是可以多次嵌套调用的,如下所示的代码就展示了如何在 defer 函数中多次调用 panic。

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码func main() {
defer fmt.Println("in main 1")
defer func() {
defer func() {
panic("panic again and again")
}()
panic("panic again")
}()

defer fmt.Println("in main 2")

panic("panic once")
}
1
2
3
4
5
6
7
8
9
10
go复制代码$ go run main.go
in main 2
in main 1
panic: panic once
panic: panic again
panic: panic again and again

goroutine 1 [running]:
...
exit status 2

程序多次调用 panic 也不会影响 defer 函数的正常执行,所以使用 defer 进行收尾工作一般来说都是安全的。

小结

分析程序的崩溃和恢复过程比较棘手,代码不是特别容易理解。

  1. 编译器会负责做转换关键字的工作;
    1. 将 panic 和 recover 分别转换成 runtime.gopanic 和 runtime.gorecover;
    2. 将 defer 转换成 runtime.deferproc 函数;
    3. 在调用 defer 的函数末尾调用 runtime.deferreturn 函数;
  2. 在运行过程中遇到 runtime.gopanic 方法时,会从 Goroutine 的链表依次取出 runtime._defer 结构体并执行;
  3. 如果调用延迟执行函数时遇到了 runtime.gorecover 就会将 _panic.recovered 标记成 true 并返回 panic 的参数;
    1. 在这次调用结束之后,runtime.gopanic 会从 runtime._defer 结构体中取出程序计数器 pc 和栈指针 sp 并调用 runtime.recovery 函数进行恢复程序;
    2. runtime.recovery 会根据传入的 pc 和 sp 跳转回 runtime.deferproc;
    3. 编译器自动生成的代码会发现 runtime.deferproc 的返回值不为 0,这时会跳回 runtime.deferreturn 并恢复到正常的执行流程;
  4. 如果没有遇到 runtime.gorecover 就会依次遍历所有的 runtime._defer,并在最后调用 runtime.fatalpanic 中止程序、打印 panic 的参数并返回错误码 2;

参阅

  • Go 语言设计与实现

本文转载自: 掘金

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

Java基础学习22Set集合 Java基础学习22Set集

发表于 2021-11-23

Java基础学习22Set集合

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

关于作者

  • 作者介绍

🍓 博客主页:作者主页

🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆

🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨‍💻


Set接口简介

Set接口与List接口最大的区别就是内容是不允许重复的,通知Set和List最大的区别还有一个就是

set接口没有对Collection接口进行扩充,而List对Collection接口进行了扩充。由于jdk1.8的原因,所以在collection接口也提供有一些default方法,而这写方法并没有在Set接口里面出现。也就是说set接口里面不可能使用get()方法进行处理,而在Set子接口里面有我们常用的子类HashSet、TreeSet。

image-20210830135859831

散列存放的子类:HashSet

Hash(哈希)属于一种算法,这种算法的核心意义指的是找空保存算法,所以只要一看见Hash就是说没有顺序的保存。

观察Set接口的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package com.day17.demo;

import java.util.HashSet;
import java.util.Set;

public class HashSetDemo {
public static void main(String[] args) {
Set<String> all = new HashSet<>();
all.add("Hello");
all.add("zsr");
all.add("zsr");
all.add("Abc");
System.out.println(all);
}
}

保存数据在输出之后发现,重复的数据没有了,并且其本身的保存也是没有任何顺序的。

排序存放的子类:TreeSet

如果现在希望Set集合之中保存的数据有顺序,那么就通过TreeSet进行Set接口的实例化。

使用TreeSet实例化接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
JAVA复制代码package com.day17.demo;

import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class HashSetDemo {
public static void main(String[] args) {
Set<String> all = new TreeSet<>();
all.add("C");
all.add("C");
all.add("A");
all.add("B");
all.add("D");
System.out.println(all);
}
}

现在发现所有的保存的数据没有重复且有顺序。TreeSet使用的是一个升序排列的模式完成的。

关于TreeSet排序的说明

通过之前的程序可以发现,使用TreeSet实例化Set接口之中,所有保存的数据都是有序的,那么在这种情况下,那么如果说使用一个自定义的类呢?

那么这个时候如果这个类的对象要进行排序的话,则这个类必须实现Comparable接口,设置比较规则,但是在这种情况下有一点必须注意:一旦使用了Comparable的话,类之中所有的属性都必须写进排序规则。

自定义排序

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
java复制代码package com.day17.demo;

import java.util.Set;
import java.util.TreeSet;
class Personn implements Comparable<Personn>{
private String name;
private Integer age;

public Personn(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "person [name=" + this.name + ", age=" + this.age + "]\n";
}
@Override
public int compareTo(Personn o) {
// TODO Auto-generated method stub
if(this.age > o.age){
return 1;
}else if (this.age < o.age){
return 0;
}else{
return this.name.compareTo(o.name);
}

}

}
public class TreeSetDemo {
public static void main(String[] args) {
// TODO 自动生成的方法存根
Set<Personn> all=new TreeSet<Personn>();
all.add(new Personn("张三",20));
all.add(new Personn("张三",20));
all.add(new Personn("李四",20));
all.add(new Personn("王五",30));
all.add(new Personn("赵六",40));
System.out.println(all);
}
}

因为在实际开发之中TreeSet的使用实在是过于麻烦了,在项目开发的简单java类是根据数据库表的设计而来的,如果一张数据表的字段过多,你这个类得写死。

image-20210830180601214

TreeSet子类依靠Compara()方法的返回值是否为0来判断是否为重复元素.

关于重复元素的说明

在使用TreeSet子类进行数据保存的时候,重复元素的判断依靠的是Comparable接口完成的。这并不是全部Set接口判断重复元素的方式,因为如果使用的是HashSet子类,由于其Comparable没有任何关系,所以他判断重复重复元素的主要依靠的是两个方法:

  • hash码:public int hashCode();
  • 对象比较:public Boolean equals(Object obj)

在进行对象比较的过程之中,首先会使用hashCode()与保存在集合之中的对象的hashCode()进行比较,如果代码相同,则再使用equals()方法进行内容的比较,如果全部相同,则为相同元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
java复制代码package com.day17.demo;

import java.util.Set;
import java.util.TreeSet;
class Personn implements Comparable<Personn>{
private String name;
private Integer age;

public Personn(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((age == null) ? 0 : age.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Personn other = (Personn) obj;
if (age == null) {
if (other.age != null)
return false;
} else if (!age.equals(other.age))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public String toString() {
return "person [name=" + this.name + ", age=" + this.age + "]\n";
}
@Override
public int compareTo(Personn o) {
// TODO Auto-generated method stub
if(this.age > o.age){
return 1;
}else if (this.age < o.age){
return 0;
}else{
return this.name.compareTo(o.name);
}

}

}
public class TreeSetDemo {
public static void main(String[] args) {
// TODO 自动生成的方法存根
Set<Personn> all=new TreeSet<Personn>();
all.add(new Personn("张三",20));
all.add(new Personn("张三",20));
all.add(new Personn("李四",20));
all.add(new Personn("王五",30));
all.add(new Personn("赵六",40));
System.out.println(all);
}
}

​ 如果要想标识出对象的唯一性,一定需要hashCode()、equals()共同作用。

面试题:如果两个hashCode()相同、equals()不同结果是什么?不能消除

面试题:如果两个hashCode()不相同、equals()相同结果是什么?不能消除

​ 对象判断必须两个都要实现。

image-20210830200214061

集合的输出操作

在之前所介绍的都属于单值集合的基本操作,可是对于集合有一个最重要的问题就是如何进行集合内容的输出操作,而这个问题在Java的类集框架之中给出了四种输出方式:Iterator,ListIterator,Enumeration,foreach。

迭代输出:Iterator

image-20210830200741889

public boolean hasNext() 判断是否有下一个元素
public E next() 取得当前元素
public default void remove() 删除元素

标准Iterator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码package com.day17.demo;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class ArrayListDemo {
public static void main(String[] args) {
List<String> all = new ArrayList<>();//此时集合里面只适合保存String类型数据
all.add("Hello");
all.add("Hello"); //重复数据
all.add("world~!");
all.add("zsr~");
Iterator<String> iter = all.iterator();//实例化Iterator
while(iter.hasNext()){
String str = iter.next();
System.out.println(str);
}
}
}

对于Iterator接口中提供的remove()方法主要解决的就是集合内元素删除的问题

remove操作

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
java复制代码package com.day17.demo;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class ArrayListDemo {
public static void main(String[] args) {
List<String> all = new ArrayList<>();//此时集合里面只适合保存String类型数据
all.add("Hello");
all.add("Hello"); //重复数据
all.add("a");
all.add("world~!");
Iterator<String> iter = all.iterator();//实例化Iterator
while(iter.hasNext()){
String str = iter.next();
if("a".equals(str)){
all.remove("a");//如果使用此操作后面的中断执行了
//iter.remove();//如果不中断后续输出,则执行
continue ;
}
System.out.println(str);
}
}
}

以后只要是见到了集合的输出操作,永远都是用Iterator接口完成。

双向迭代输出:ListIterator

Iterator可以完成的是由前向后的单项输出操作,如果现在希望可以完成由前向后,由后向前的输出的话,那么就可以利用ListIterator接口完成,此接口是Iterator的子接口,在ListIterator接口主要使用一下两个扩充方法:

判断是否有前一个元素:public Boolean hasPrevious();

取出前一个元素:public E previous()。

但是如果要想取得ListIterator接口的实例化对象,Collection没有这样的方法支持,这个方法在List接口之中存在:

Public ListIterator listIterator()

执行双向迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码package com.day17.demo;


import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;


public class ListIteratorDemo {
public static void main(String[] args) {
// TODO 自动生成的方法存根
List<String> all=new ArrayList<String>();
all.add("hello");
all.add("hello");
all.add("world");
ListIterator<String> ite=all.listIterator();
System.out.println("由前向后");
while(ite.hasNext()){
String str=ite.next();
System.out.print(str + "、");
}
System.out.println();
System.out.println("由后向前");
while(ite.hasPrevious()){
String str=ite.previous();
System.out.print(str + "、");
}
}
}

但是对于由后向前的操作,在进行之前一定发生由前向后的输出。由于此输出接口只有List可以使用,所以在开发之中几乎不会出现。

废弃的接口:Enumeration

Enumeration是一个最早的输出接口,最早成为枚举输出,在JDK1.0的时候就已经推出了,并且在JDK1.5的时候将其功能进行扩充,主要就是增加了泛型,在Enumeration接口里只定义了两个方法:

判断是否有下一个元素:public Boolean hasMoreElements()

取得当前元素:public E nextElement()

不过要想取得Enumeration的实例化对象,不能依靠Collection接口了,之能够依靠Vector类完成,在Vector子类之中定义了一个方法:public Enumeration elements()。

使用Enumertaion进行输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码package com.day17.demo;

import java.util.Enumeration;
import java.util.Vector;

public class IteratorTest {
public static void main(String[] args) {
// TODO 自动生成的方法存根
Vector<String> all=new Vector<String>();
all.add("hello");
all.add("hello");
all.add("world");
Enumeration<String> ite=all.elements();
while(ite.hasMoreElements()){
String str=ite.nextElement();
System.out.println(str);
}
}
}

从开发而言,首先考虑的绝对不是Enumeration,考虑的肯定是Iterator,只有在必须使用的时候才用它。

JDK1.5的支持foreach

对于foreach输出除了可以进行数组的输出以外,还可以进行集合的输出。

使用foreach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码package com.day17.demo;

import java.util.ArrayList;
import java.util.List;

public class IteratorTest {
public static void main(String[] args) {
// TODO 自动生成的方法存根
List<String> all=new ArrayList<String>();
all.add("hello");
all.add("hello");
all.add("world");
for(String x : all){
System.out.println(x);
}
}
}

使用foreach并不是一个被广泛认可的操作代码形式。

本文转载自: 掘金

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

lombok注解背后的原理是什么,让我们走近自定义Java

发表于 2021-11-23

本文介绍了如何自定义Java注解处理器及涉及到的相关知识,看完本文可以很轻松看懂并理解各大开源框架的注解处理器的应用。

《游园不值》
应怜屐齿印苍苔 ,小扣柴扉久不开 。
春色满园关不住 ,一枝红杏出墙来 。
-宋,叶绍翁

本文首发:yuweiguocn.github.io/

关于自定义Java注解请查看自定义注解。

本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发。

基本实现

实现一个自定义注解处理器需要有两个步骤,第一是实现Processor接口处理注解,第二是注册注解处理器。

实现Processor接口

通过实现Processor接口可以自定义注解处理器,这里我们采用更简单的方法通过继承AbstractProcessor类实现自定义注解处理器。实现抽象方法process处理我们想要的功能。

1
2
3
4
5
6
scala复制代码public class CustomProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
return false;
}
}

除此之外,我们还需要指定支持的注解类型以及支持的Java版本通过重写getSupportedAnnotationTypes方法和getSupportedSourceVersion方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript复制代码public class CustomProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotataions = new LinkedHashSet<String>();
annotataions.add(CustomAnnotation.class.getCanonicalName());
return annotataions;
}

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}

对于指定支持的注解类型,我们还可以通过注解的方式进行指定:

1
2
3
4
5
6
7
8
9
10
11
scala复制代码@SupportedAnnotationTypes({"io.github.yuweiguocn.annotation.CustomAnnotation"})
public class CustomProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
return false;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}

因为Android平台可能会有兼容问题,建议使用重写getSupportedAnnotationTypes方法指定支持的注解类型。

注册注解处理器

最后我们还需要将我们自定义的注解处理器进行注册。新建res文件夹,目录下新建META-INF文件夹,目录下新建services文件夹,目录下新建javax.annotation.processing.Processor文件,然后将我们自定义注解处理器的全类名写到此文件:

1
lua复制代码io.github.yuweiguocn.processor.CustomProcessor

上面这种注册的方式太麻烦了,谷歌帮我们写了一个注解处理器来生成这个文件。
github地址:github.com/google/auto
添加依赖:

1
python复制代码compile 'com.google.auto.service:auto-service:1.0-rc2'

添加注解:

1
2
3
4
scala复制代码@AutoService(Processor.class)
public class CustomProcessor extends AbstractProcessor {
...
}

搞定,体会到注解处理器的强大木有。后面我们只需关注注解处理器中的处理逻辑即可。

我们来看一下最终的项目结构:

基本概念

抽象类中还有一个init方法,这是Processor接口中提供的一个方法,当我们编译程序时注解处理器工具会调用此方法并且提供实现ProcessingEnvironment接口的对象作为参数。

1
2
3
4
java复制代码@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}

我们可以使用ProcessingEnvironment获取一些实用类以及获取选项参数等:

方法 说明
Elements getElementUtils() 返回实现Elements接口的对象,用于操作元素的工具类。
Filer getFiler() 返回实现Filer接口的对象,用于创建文件、类和辅助文件。
Messager getMessager() 返回实现Messager接口的对象,用于报告错误信息、警告提醒。
Map<String,String> getOptions() 返回指定的参数选项。
Types getTypeUtils() 返回实现Types接口的对象,用于操作类型的工具类。

元素

Element元素是一个接口,表示一个程序元素,比如包、类或者方法。以下元素类型接口全部继承自Element接口:

类型 说明
ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素。
PackageElement 表示一个包程序元素。提供对有关包及其成员的信息的访问。
TypeElement 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。
TypeParameterElement 表示一般类、接口、方法或构造方法元素的形式类型参数。
VariableElement 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。

如果我们要判断一个元素的类型,应该使用Element.getKind()方法配合ElementKind枚举类进行判断。尽量避免使用instanceof进行判断,因为比如TypeElement既表示类又表示一个接口,这样判断的结果可能不是你想要的。例如我们判断一个元素是不是一个类:

1
2
3
4
5
6
scss复制代码if (element instanceof TypeElement) { //错误,也有可能是一个接口
}

if (element.getKind() == ElementKind.CLASS) { //正确
//doSomething
}

下表为ElementKind枚举类中的部分常量,详细信息请查看官方文档。

类型 说明
PACKAGE 一个包。
ENUM 一个枚举类型。
CLASS 没有用更特殊的种类(如 ENUM)描述的类。
ANNOTATION_TYPE 一个注解类型。
INTERFACE 没有用更特殊的种类(如 ANNOTATION_TYPE)描述的接口。
ENUM_CONSTANT 一个枚举常量。
FIELD 没有用更特殊的种类(如 ENUM_CONSTANT)描述的字段。
PARAMETER 方法或构造方法的参数。
LOCAL_VARIABLE 局部变量。
METHOD 一个方法。
CONSTRUCTOR 一个构造方法。
TYPE_PARAMETER 一个类型参数。

类型

TypeMirror是一个接口,表示 Java 编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和 null 类型。还可以表示通配符类型参数、executable 的签名和返回类型,以及对应于包和关键字 void 的伪类型。以下类型接口全部继承自TypeMirror接口:

类型 说明
ArrayType 表示一个数组类型。多维数组类型被表示为组件类型也是数组类型的数组类型。
DeclaredType 表示某一声明类型,是一个类 (class) 类型或接口 (interface) 类型。这包括参数化的类型(比如 java.util.Set)和原始类型。TypeElement 表示一个类或接口元素,而 DeclaredType 表示一个类或接口类型,后者将成为前者的一种使用(或调用)。
ErrorType 表示无法正常建模的类或接口类型。
ExecutableType 表示 executable 的类型。executable 是一个方法、构造方法或初始化程序。
NoType 在实际类型不适合的地方使用的伪类型。
NullType 表示 null 类型。
PrimitiveType 表示一个基本类型。这些类型包括 boolean、byte、short、int、long、char、float 和 double。
ReferenceType 表示一个引用类型。这些类型包括类和接口类型、数组类型、类型变量和 null 类型。
TypeVariable 表示一个类型变量。
WildcardType 表示通配符类型参数。

同样,如果我们想判断一个TypeMirror的类型,应该使用TypeMirror.getKind()方法配合TypeKind枚举类进行判断。尽量避免使用instanceof进行判断,因为比如DeclaredType既表示类 (class) 类型又表示接口 (interface) 类型,这样判断的结果可能不是你想要的。

TypeKind枚举类中的部分常量,详细信息请查看官方文档。

类型 说明
BOOLEAN 基本类型 boolean。
INT 基本类型 int。
LONG 基本类型 long。
FLOAT 基本类型 float。
DOUBLE 基本类型 double。
VOID 对应于关键字 void 的伪类型。
NULL null 类型。
ARRAY 数组类型。
PACKAGE 对应于包元素的伪类型。
EXECUTABLE 方法、构造方法或初始化程序。

创建文件

Filer接口支持通过注解处理器创建新文件。可以创建三种文件类型:源文件、类文件和辅助资源文件。

1.创建源文件

1
2
3
java复制代码JavaFileObject createSourceFile(CharSequence name,
Element... originatingElements)
throws IOException

创建一个新的源文件,并返回一个对象以允许写入它。文件的名称和路径(相对于源文件的根目录输出位置)基于该文件中声明的类型。如果声明的类型不止一个,则应该使用主要顶层类型的名称(例如,声明为 public 的那个)。还可以创建源文件来保存有关某个包的信息,包括包注解。要为指定包创建源文件,可以用 name 作为包名称,后跟 “.package-info”;要为未指定的包创建源文件,可以使用 “package-info”。

2.创建类文件

1
2
3
java复制代码JavaFileObject createClassFile(CharSequence name,
Element... originatingElements)
throws IOException

创建一个新的类文件,并返回一个对象以允许写入它。文件的名称和路径(相对于类文件的根目录输出位置)基于将写入的类型名称。还可以创建类文件来保存有关某个包的信息,包括包注解。要为指定包创建类文件,可以用 name 作为包名称,后跟 “.package-info”;为未指定的包创建类文件不受支持。

3.创建辅助资源文件

1
2
3
4
5
java复制代码FileObject createResource(JavaFileManager.Location location,
CharSequence pkg,
CharSequence relativeName,
Element... originatingElements)
throws IOException

创建一个用于写入操作的新辅助资源文件,并为它返回一个文件对象。该文件可以与新创建的源文件、新创建的二进制文件或者其他受支持的位置一起被查找。位置 CLASS_OUTPUT 和 SOURCE_OUTPUT 必须受支持。资源可以是相对于某个包(该包是源文件和类文件)指定的,并通过相对路径名从中取出。从不太严格的角度说,新文件的完全路径名将是 location、 pkg 和 relativeName 的串联。

对于生成Java文件,还可以使用Square公司的开源类库JavaPoet,感兴趣的同学可以了解下。

打印错误信息

Messager接口提供注解处理器用来报告错误消息、警告和其他通知的方式。

注意:我们应该对在处理过程中可能发生的异常进行捕获,通过Messager接口提供的方法通知用户。此外,使用带有Element参数的方法连接到出错的元素,用户可以直接点击错误信息跳到出错源文件的相应行。如果你在process()中抛出一个异常,那么运行注解处理器的JVM将会崩溃(就像其他Java应用一样),这样用户会从javac中得到一个非常难懂出错信息。

方法 说明
void printMessage(Diagnostic.Kind kind, CharSequence msg) 打印指定种类的消息。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e) 在元素的位置上打印指定种类的消息。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a) 在已注解元素的注解镜像位置上打印指定种类的消息。
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a, AnnotationValue v) 在已注解元素的注解镜像内部注解值的位置上打印指定种类的消息。

配置选项参数

我们可以通过getOptions()方法获取选项参数,在gradle文件中配置选项参数值。例如我们配置了一个名为yuweiguoCustomAnnotation的参数值。

1
2
3
4
5
6
7
8
9
ini复制代码android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [ yuweiguoCustomAnnotation : 'io.github.yuweiguocn.customannotation.MyCustomAnnotation' ]
}
}
}
}

在注解处理器中重写getSupportedOptions方法指定支持的选项参数名称。通过getOptions方法获取选项参数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typescript复制代码public static final String CUSTOM_ANNOTATION = "yuweiguoCustomAnnotation";

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
String resultPath = processingEnv.getOptions().get(CUSTOM_ANNOTATION);
if (resultPath == null) {
...
return false;
}
...
} catch (Exception e) {
e.printStackTrace();
...
}
return true;
}

@Override
public Set<String> getSupportedOptions() {
Set<String> options = new LinkedHashSet<String>();
options.add(CUSTOM_ANNOTATION);
return options;
}

处理过程

Java官方文档给出的注解处理过程的定义:注解处理过程是一个有序的循环过程。在每次循环中,一个处理器可能被要求去处理那些在上一次循环中产生的源文件和类文件中的注解。第一次循环的输入是运行此工具的初始输入。这些初始输入,可以看成是虚拟的第0次的循环的输出。这也就是说我们实现的process方法有可能会被调用多次,因为我们生成的文件也有可能会包含相应的注解。例如,我们的源文件为SourceActivity.class,生成的文件为Generated.class,这样就会有三次循环,第一次输入为SourceActivity.class,输出为Generated.class;第二次输入为Generated.class,输出并没有产生新文件;第三次输入为空,输出为空。

每次循环都会调用process方法,process方法提供了两个参数,第一个是我们请求处理注解类型的集合(也就是我们通过重写getSupportedAnnotationTypes方法所指定的注解类型),第二个是有关当前和上一次 循环的信息的环境。返回值表示这些注解是否由此 Processor 声明,如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们。

1
2
typescript复制代码public abstract boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv)

获取注解元素

我们可以通过RoundEnvironment接口获取注解元素。process方法会提供一个实现RoundEnvironment接口的对象。

方法 说明
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a) 返回被指定注解类型注解的元素集合。
Set<? extends Element> getElementsAnnotatedWith(TypeElement a) 返回被指定注解类型注解的元素集合。
processingOver() 如果循环处理完成返回true,否则返回false。

示例

了解完了相关的基本概念,接下来我们来看一个示例,本示例只为演示无实际意义。主要功能为自定义一个注解,此注解只能用在public的方法上,我们通过注解处理器拿到类名和方法名存储到List集合中,然后生成通过参数选项指定的文件,通过此文件可以获取List集合。

自定义注解:

1
2
3
4
5
less复制代码@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotation {
}

注解处理器中关键代码:

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
java复制代码@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
String resultPath = processingEnv.getOptions().get(CUSTOM_ANNOTATION);
if (resultPath == null) {
messager.printMessage(Diagnostic.Kind.ERROR, "No option " + CUSTOM_ANNOTATION +
" passed to annotation processor");
return false;
}

round++;
messager.printMessage(Diagnostic.Kind.NOTE, "round " + round + " process over " + roundEnv.processingOver());
Iterator<? extends TypeElement> iterator = annotations.iterator();
while (iterator.hasNext()) {
messager.printMessage(Diagnostic.Kind.NOTE, "name is " + iterator.next().getSimpleName().toString());
}

if (roundEnv.processingOver()) {
if (!annotations.isEmpty()) {
messager.printMessage(Diagnostic.Kind.ERROR,
"Unexpected processing state: annotations still available after processing over");
return false;
}
}

if (annotations.isEmpty()) {
return false;
}

for (Element element : roundEnv.getElementsAnnotatedWith(CustomAnnotation.class)) {
if (element.getKind() != ElementKind.METHOD) {
messager.printMessage(
Diagnostic.Kind.ERROR,
String.format("Only methods can be annotated with @%s", CustomAnnotation.class.getSimpleName()),
element);
return true; // 退出处理
}

if (!element.getModifiers().contains(Modifier.PUBLIC)) {
messager.printMessage(Diagnostic.Kind.ERROR, "Subscriber method must be public", element);
return true;
}

ExecutableElement execElement = (ExecutableElement) element;
TypeElement classElement = (TypeElement) execElement.getEnclosingElement();
result.add(classElement.getSimpleName().toString() + "#" + execElement.getSimpleName().toString());
}
if (!result.isEmpty()) {
generateFile(resultPath);
} else {
messager.printMessage(Diagnostic.Kind.WARNING, "No @CustomAnnotation annotations found");
}
result.clear();
} catch (Exception e) {
e.printStackTrace();
messager.printMessage(Diagnostic.Kind.ERROR, "Unexpected error in CustomProcessor: " + e);
}
return true;
}

private void generateFile(String path) {
BufferedWriter writer = null;
try {
JavaFileObject sourceFile = filer.createSourceFile(path);
int period = path.lastIndexOf('.');
String myPackage = period > 0 ? path.substring(0, period) : null;
String clazz = path.substring(period + 1);
writer = new BufferedWriter(sourceFile.openWriter());
if (myPackage != null) {
writer.write("package " + myPackage + ";\n\n");
}
writer.write("import java.util.ArrayList;\n");
writer.write("import java.util.List;\n\n");
writer.write("/** This class is generated by CustomProcessor, do not edit. */\n");
writer.write("public class " + clazz + " {\n");
writer.write(" private static final List<String> ANNOTATIONS;\n\n");
writer.write(" static {\n");
writer.write(" ANNOTATIONS = new ArrayList<>();\n\n");
writeMethodLines(writer);
writer.write(" }\n\n");
writer.write(" public static List<String> getAnnotations() {\n");
writer.write(" return ANNOTATIONS;\n");
writer.write(" }\n\n");
writer.write("}\n");
} catch (IOException e) {
throw new RuntimeException("Could not write source for " + path, e);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
//Silent
}
}
}
}

private void writeMethodLines(BufferedWriter writer) throws IOException {
for (int i = 0; i < result.size(); i++) {
writer.write(" ANNOTATIONS.add(\"" + result.get(i) + "\");\n");
}
}

编译输出:

1
2
3
4
vbnet复制代码Note: round 1 process over false
Note: name is CustomAnnotation
Note: round 2 process over false
Note: round 3 process over true

获取完整代码:github.com/yuweiguocn/…

关于上传自定义注解处理器到jcenter中,请查看上传类库到jcenter。

很高兴你能阅读到这里,此时再去看EventBus 3.0中的注解处理器的源码,相信你可以很轻松地理解它的原理。

注意:如果你clone了工程代码,你可能会发现注解和注解处理器是单独的module。有一点可以肯定的是我们的注解处理器只需要在编译的时候使用,并不需要打包到APK中。因此为了用户考虑,我们需要将注解处理器分离为单独的module。

参考

  • www.race604.com/annotation-…
  • docs.oracle.com/javase/7/do…
  • tool.oschina.net/uploads/api…
  • github.com/greenrobot/…
  • hannesdorfmann.com/annotation-…
  • github.com/sockeqwe/an…

作者:于卫国
链接:www.jianshu.com/p/50d95fbf6…
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

本文转载自: 掘金

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

分库分表技术之MyCat(4)

发表于 2021-11-23

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

MyCat分库分表

分片规则配置(水平分库)

  • 水平分库:把一张表的数据按照一定规则,分配到不同的数据库,每一个库只有这张表的部分数据。
  • 在rule.xml配置,自动分片
    • 每个datanode中保存一定数量的数据。根据id进行分片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码
<!-- schema 逻辑库 -->
<schema name="rgtest" checkSQLschema="true" sqlMaxLimit="100" >
<table name="pay_order" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" primaryKey="id" autoIncrement="true" >
</table>
</schema>


<!-- 自动分片 -->
<tableRule name="auto-sharding-long">
<rule>
<columns>id</columns>
<algorithm>rang-long</algorithm>
</rule>
</tableRule>

<function name="rang-long" class="io.mycat.route.function.AutoPartitionByLong">
<property name="mapFile">autopartition-long.txt</property>
</function>
  • autopartition-long.txt
1
2
3
4
5
text复制代码# range start-end ,data node index
# K=1000,M=10000.
0-1k=0
1k-2k=1
2k-3k=2
  • 对分片规则进行测试id范围为:
  • Datanode1*:1~1000
  • Datanode2*:1000~2000
  • Datanode3*:2000~3000

启动MyCat 进行测试

  • 重启MyCat
1
2
text复制代码停止命令:./mycat stop
重启命令:./mycat restart
  • 在MyCat中创建逻辑表
1
2
3
4
5
6
7
8
mysql复制代码DROP TABLE IF EXISTS pay_order;

CREATE TABLE pay_order (
id BIGINT(20) PRIMARY KEY,
user_id INT(11) NOT NULL ,
product_name VARCHAR(128) ,
amount DECIMAL(12,2)
);
  • MyCat中创建好表之后,我们的MySQL节点中也会对应的创建表
  • 插入数据,观察数据被插入到哪张表中
1
mysql复制代码INSERT INTO pay_order(id,user_id,product_name,amount) VALUES(2001,1,"面试宝典",15.8);
  • 注意: 解决MyCat乱码问题

全局序列号

在实现分库分表的情况下,数据库自增主键已无法保证自增主键的全局唯一。为此,Mycat提供了全局sequence,并且提供了包含本地配置和数据库配置等多种实现方式。

  • server.xml文件中
1
2
3
xml复制代码<system>
<property name="sequnceHandlerType">0</property>
</system>
1
2
3
text复制代码0 表示是表示使用本地文件方式。
1 表示的是根据数据库来生成
2 表示时间戳的方式 ID= 64 位二进制 (42(毫秒)+5(机器 ID)+5(业务编码)+12(重复累加)

本地文件

此方式Mycat将sequence配置到文件中,当使用到sequence中的配置后,Mycat会更新classpath中的sequence_conf.properties文件中sequence当前的值。

1
2
3
4
text复制代码PAY_ORDER.HISIDS=
PAY_ORDER.MINID=101
PAY_ORDER.MAXID=10000000
PAY_ORDER.CURID=100

其中HISIDS表示使用过的历史分段(一般无特殊需要可不配置),MINID表示最小ID值,MAXID表示最大ID值,CURID 表示当前ID值

重启MyCat,插入一条数据,不用指定id

1
mysql复制代码INSERT INTO pay_order(user_id,product_name,amount) VALUES(1,"xiao",12.8);

MyCat读写分离

什么是读写分离

在实际的生产环境中,数据的读写操作如果都在同一个数据库服务器中进行,当遇到大量的并发读或者写操作的时候,是没有办法满足实际需求的,数据库的吞吐量将面临巨大的瓶颈压力。

  • 主从复制
    通过搭建主从架构,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
  • 读写分离
    读写分离就是让主库处理事务性操作,从库处理select查询。数据库复制被用来把事务性查询导致的数据变更同步到从库,同时主库也可以select查询。

读写分离的数据节点中的数据内容是一致。

image.png

MySQL主从复制(同步)

MyCat的读写分离是建立在MySQL主从复制基础之上实现的,所以必须先搭建MySQL的主从复制架构。

image.png

主从复制的用途

  • 实时灾备,用于故障切换
  • 读写分离,提供查询服务
  • 备份,避免影响业务

主从部署必要条件

  • 主库开启binlog日志(设置log-bin参数)
  • 主从server-id不同
  • 从库服务器能连通主库

主从复制的原理

  • Mysql中有一种日志叫做bin日志(二进制日志)。这个日志会记录下所有修改了数据库的SQL语句(insert,update,delete,create/alter/drop table,grant等等)。
  • 主从复制的原理其实就是把主服务器上的bin日志复制到从服务器上执行一遍,这样从服务器上的数据就和主服务器上的数据相同了。
    • 主库db的更新事件(update、insert、delete)被写到binlog
    • 主库创建一个binlog dump thread,把binlog的内容发送到从库
    • 从库启动并发起连接,连接到主库
    • 从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log
    • 从库启动之后,创建一个SQL线程,从relay log里面读取内容,执行读取到的更新事件,将更新内容写入到slave的db

主从复制架构搭建

Mysql的主从复制至少是需要两个Mysql的服务,当然Mysql的服务是可以分布在不同的服务器上,也可以在一台服务器上启动多个服务。

第一步 master中创建数据库和表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql复制代码-- 创建数据库
CREATE DATABASE test CHARACTER SET utf8;

-- 创建表
CREATE TABLE users (
id INT(11) PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(20) DEFAULT NULL,
age INT(11) DEFAULT NULL
);

-- 插入数据
INSERT INTO users VALUES(NULL,'user1',20);
INSERT INTO users VALUES(NULL,'user2',21);
INSERT INTO users VALUES(NULL,'user3',22);

第二步 修改主数据库的配置文件my.cnf

1
bash复制代码vim /etc/my.cnf

插入下面的内容

1
2
3
4
5
6
ini复制代码lower_case_table_names=1

log-bin=mysql-bin
server-id=1
binlog-do-db=test
binlog_ignore_db=mysql
  • server-id=1中的1可以任定义,只要是唯一的就行
  • log-bin=mysql-bin表示启用binlog功能,并制定二进制日志的存储目录
  • binlog-do-db=test是表示只备份test数据库
  • binlog_ignore_db=mysql表示忽略备份mysql
  • 不加binlog-do-db和binlog_ignore_db,那就表示备份全部数据库

第三步 重启MySQL

1
复制代码service mysqld restart

第四步 在主数据库上, 创建一个允许从数据库来访问的用户账号

用户:slave

密码:123456

主从复制使用REPLICATION SLAVE赋予权限

1
2
mysql复制代码-- 创建账号
GRANT REPLICATION SLAVE ON *.* TO 'slave'@'192.168.52.11' IDENTIFIED BY 'Qwer@1234';

第五步 停止主数据库的更新操作,并且生成主数据库的备份

1
2
mysql复制代码-- 执行以下命令锁定数据库以防止写入数据。
FLUSH TABLES WITH READ LOCK;

导出数据库,恢复写操作

使用SQLYog导出,主数据库备份完毕,恢复写操作

1
ini复制代码unlock tables;

将刚才主数据库备份的test.sql导入到从数据库

导入后,主库和从库数据会追加相平,保持同步!此过程中,若主库存在业务,并发较高,在同步的时候要先锁表,让其不要有修改!等待主从数据追平,主从同步后在打开锁!

接着修改从数据库的my.cnf

  • 增加server-id参数,保证唯一
1
ini复制代码server-id=2
1
2
diff复制代码-- 重启
service mysqld restart

在从数据库设置相关信息

  • 执行以下SQL
1
2
3
4
5
6
7
8
9
ini复制代码STOP SLAVE;

CHANGE MASTER TO MASTER_HOST='192.168.52.10',
MASTER_USER='slave',
MASTER_PASSWORD='Qwer@1234',
MASTER_PORT=3306,
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=0,
MASTER_CONNECT_RETRY=10;

修改auto.cnf中的UUID,保证唯一

1
2
3
4
5
6
7
8
diff复制代码-- 编辑auto.cnf
vim /var/lib/mysql/auto.cnf

-- 修改UUID的值
server-uuid=a402ac7f-c392-11ea-ad18-000c2980a208

-- 重启
service mysqld restart

在从服务器上,启动slave进程

1
2
3
4
5
6
7
sql复制代码start slave;

-- 查看状态
SHOW SLAVE STATUS;

-- 命令行下查看状态 执行
SHOW SLAVE STATUS \G;

注意:这两个参数的值,必须是Yes,否则就要进行错误的排查。

现在可以在我们的主服务器做一些更新的操作,然后在从服务器查看是否已经更新

1
2
mysql复制代码-- 在主库插入一条数据,观察从库是否同步
INSERT INTO users VALUES(NULL,'user4',23);

实现读写分离

数据库读写分离对于大型系统或者访问量很高的互联网应用来说,是必不可少的一个重要功能。对于MySQL来说,标准的读写分离是主从模式,一个写节点Master后面跟着多个读节点,读节点的数量取决于系统的压力,通常是1-3个读节点的配置

image.png

在schema.xml文件中配置Mycat读写分离。使用前需要搭建MySQL主从架构,并实现主从复制,Mycat不负责数据同步问题。

  • server.xml

修改用户可以访问的逻辑表为test

1
2
3
4
5
xml复制代码<user name="root" defaultAccount="true">
<property name="password">123456</property>
<property name="schemas">test</property>
<property name="defaultSchema">test</property>
</user>
  • schema
    • 逻辑库name=”test”
    • 逻辑表name=”users”
    • 读写分离不设置分片规则ruleRequired=false
    • 分片节点dataNode=”dn4”
1
2
3
4
5
xml复制代码
<schema name="test" checkSQLschema="true" sqlMaxLimit="100">
<table name="users" dataNode="dn4" ruleRequired="false" primaryKey="id" autoIncrement="true" >
</table>
</schema>
  • dataNode
1
2
xml复制代码<!-- 读写分离 -->
<dataNode name="dn4" dataHost="localhost3" database="test" />
  • dataHost
1
2
3
4
5
6
7
8
9
xml复制代码<!-- 读写分离 -->
<dataHost name="localhost3" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<!-- 主 -->
<writeHost host="M1" url="192.168.52.10:3306" user="root" password="123456">
<!-- 从 -->
<readHost host="S1" url="192.168.52.11:3306" user="root" password="123456" weight="1" />
</writeHost>
</dataHost>

balance参数:

  • 0:所有读操作都发送到当前可用的writeHost
  • 1:所有读操作都随机发送到readHost和stand by writeHost
  • 2:所有读操作都随机发送到writeHost和readHost
  • 3:所有读操作都随机发送到writeHost对应的readHost上,但是writeHost不负担读压力

writeType参数:

  • 0:所有写操作都发送到可用的writeHost
  • 1:所有写操作都随机发送到readHost
  • 2:所有写操作都随机发送到writeHost,readHost
  • 重启MyCat
1
bash复制代码./mycat restart
  • 执行查询和插入操作

插入一条数据,观察否两个表都同时新增了,如果同时新增,证明插入的是主库的表

1
mysql复制代码INSERT INTO users(NAME,age) VALUES('测试abc',26);

在从库插入一条数据,然后进行查询,查询的是从库中的数据,证明查询操作在从库进行

1
mysql复制代码SELECT * FROM users;

本文转载自: 掘金

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

记录下经常忘记的位运算--与或非

发表于 2021-11-23

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

image.png

一,二进制和十进制转换方法

1. 十进制转二进制

image.png

2. 二进制转10进制

image.png

二,基本概念

1. 机器数

一个数在计算机中的二进制表示形式, 叫做这个数的机器数。

机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1。

比如,十进制中的数 6,计算机字长为8位,转换成二进制就是00000110。如果是 -6 ,就是 10000110 。这里的 00000110 和 10000110就是机器数。

2. 真值

第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 10000110,其最高位1代表负,其真正数值是 -6 而不是形式值134(10000110转换成十进制等于134)。所以为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。

例:0000 0001的真值 = +000 0001 = +1,1000 0001的真值 = –000 0001 = –1

3. 原码

原码是一种计算机中对数字的二进制定点表示方法。原码表示在数值前面增加了一位符号位(即最高位为符号位):正数该位为0,负数该位为1(0有两种表示:+0和-0),其余位表示数值的大小。 原码也是机器数的一种表示方式。

比如如果是8位二进制:

[+6] = 0000 0110

[-6] = 1000 0110

第一位是符号位. 因为第一位是符号位, 所以8位二进制数的取值范围就是:

[1111 1111 , 0111 1111]

即 [-127 , 127]

4. 反码

正数的反码与其原码相同;

负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.

[+6] = 0000 0110(原码) = 0000 0110(反码)

[-6] = 1000 0110(原码) = 1111 1001(反码)

5. 补码

正数的补码与其原码相同;

负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (补码=反码+1)

[+6] = 0000 0110(原码) = 0000 0110(反码) = 0000 0110(补码)

[-6] = 1000 0110(原码) = 1111 1001(反码) = 1111 1010(补码)

三,常用位运算

1. 与 (6 & 7)

6和7的二进制位进行与操作,只有对应的二进制位两个都为1结果才为1,否则为0

1
2
3
4
ini复制代码1 & 1 = 1
1 & 0 = 0
0 & 1 = 0
0 & 0 = 0

例如

十进制 = 二进制

6 = 110

7 = 111

6 & 7= 110

二进制110转换成十进制是6

例如:判断奇偶 6 & 0x1

2. 或(6 | 7)

6和7的二进制位进行或操作,只有对应的二进制位两个有一个为1结果就是1,否则为0

1
2
3
4
ini复制代码1 | 1 = 1
1 | 0 = 1
0 | 1 = 1
0 | 0 = 0

例如:

十进制 = 二进制

6 = 110

7 = 111

6 | 7= 111

二进制111转换成十进制是 7

3. 非(~6)

6的二进制位的每一位取反操作,二进制位如果是0,取反后是1,反之是0

1
2
ini复制代码~ 1 = 0
~ 0 = 1

在Java中整型占4字节(32个二进制位)

6 = 00000000 00000000 00000000 00000110

~6 = 11111111 11111111 11111111 11111001

上面二进制最高位是1,表示负数,而负数在计算机中是按照补码存储的。

补码-1得到反码:11111111 11111111 11111111 11111000

首位符号位不变,其他取反得到原码:10000000 00000000 00000000 00000111

二进制原码转换成十进制是 -7

4. 异或(6 ^ 7)
1
复制代码规则:两个位相同为0,不同为1

异或运算的性质

  • 任意一个变量和0异或结果都是其本身 (0^N=N)
  • 任意一个变量和其自身异或结果都是0(N^N=0)
  • 异或运算满足交换律和结合律

例1: 6 ^ 7

值 二进制
6 1 1 0
7 1 1 1
1=6^7 0 0 1
5. 左移(6<<2)

把6表示的二进制数左移2位,左边高位超出舍弃,右边低位补0;

在数字 x 上左移 y 位时,得出的结果是 x * 2^y ,即6<<2=6*2^2

6 = 00000110

6>>2 = 00011000 = 24

6. 有符号右移(6>>2)

把6表示的二进制数右移2位,向右被移出的位被丢弃,拷贝最左侧的位以填充左侧。由于新的最左侧的位总是和以前相同,符号位没有被改变。所以被称作 “符号传播”。

因为负数在内存中是以补码形式存在的,所有首先根据负数的原码求出负数的补码

正数6:

6 = 00000110

6>>2 = 00000010 = 2

负数-6:

-6右移2位

-6的原码:10000110

-6的反码:11111001 (原码符号位不变,其余按位取反)

-6的补码:11111010 (反码+1)

右移2位:11111110

补码右移不是最终结果,需要转成原码,才是最终结果

按位取反:10000001(保留符号位,其他位取反)

+1得原码:10000010

最终结果 -2

7. 无符号右移(6>>>2)

把6表示的二进制数右移2位, 向右被移出的位被丢弃,左侧用 0 填充。

因为符号位变成了 0,所以结果总是非负的。

正数6:

6 = 00000110

6>>2 = 00000010 = 2

负数-6:

-6的原码:10000000000000000000000000000110

-6的反码:11111111111111111111111111111001 (原码符号位不变,其余按位取反)

-6的补码:11111111111111111111111111111010 (反码+1)

右移2位:00111111111111111111111111111110

原码:00111111111111111111111111111110(正数的原码补码相同)

最终结果 1073741822

四,实际应用

1. 判断奇偶数
1
2
3
4
5
6
7
java复制代码int m = 6;
int n = 7;
​
System.out.println(m % 2 == 0?"偶数" : "奇数");//偶数
System.out.println(n % 2 == 0?"偶数" : "奇数");//奇数
System.out.println(((m & 1) == 1) ? "奇数" : "偶数");//偶数
System.out.println(((n & 1) == 1) ? "奇数" : "偶数"); //奇数
2. redis的bigmap和HyperLogLog

通过位运算可以做基数统计,极大的节省内存,像注册IP数、每日访问 IP 数、页面实时UV)、在线用户数,这个后面可以单独写一篇

3. 巧妙利用异或,不用额外变量交换两个变量的值
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public static void main(String[] args) {
​
           int a = 10;
           int b = 6;
      // a = a^b
           a = a ^ b;
      // b = a^b^b = a
           b = a ^ b;
      // a = a ^ a^b = b
           a = b ^ a;
           System.out.println("a="+a + ",b=" + b);  
  }

输出: a=6,b=10

4. 选择排序中交换两个数据的位置(剩下数据中最小的与当前位置互换)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public static void main(String[] args) {
       int[] nums = {5, 3, 8, 4, 2, 1, 9, 7, 6,500,100,300,200,400};
       for (int i=0;i<nums.length;i++){
           int n = i;
           for (int j=i;j<nums.length;j++){
               if (nums[n]>nums[j]){
                   n = j;
              }
          }
           if (i!=n){
               nums[i] = nums[i] ^ nums[n];
               nums[n] = nums[i] ^ nums[n];
               nums[i] = nums[i] ^ nums[n];
          }
      }
       System.out.println(Arrays.toString(nums));
​
  }

输出: [1, 2, 3, 4, 5, 6, 7, 8, 9, 100, 200, 300, 400, 500]

5. 一个数组中只有一种数出现奇数次,其他都出现偶数次,怎么找到这个数
1
2
3
4
5
6
7
8
ini复制代码public static void main(String[] args) {
       int[] nums = {3,5,5,8,8,3,10};
       int eor=0;
       for(int i=0;i<nums.length;i++){
           eor = eor^nums[i];
      }
       System.out.println("奇数="+eor);
  }

输出: 奇数=10

因为相同数字异或是0,所以其中的所有偶数的数据结果都是0,最后结果就剩下 0^10 = 10


我是纪先生,用输出倒逼输入而持续学习,持续分享技术系列文章,以及全网值得收藏好文,欢迎关注或者关注公众号,做一个持续成长的技术人。

实际问题系列的历史文章(也可以在掘金专栏中看其他相关文章)

本文转载自: 掘金

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

maven项目怎么写一个简单servlet,运行在tomca

发表于 2021-11-23

主要内容

记录下第一次使用tomcat的踩坑过程,要达到的效果为能够在tomcat上运行一个war包,这个包里包含一个servlet

前置

知道tomcat是一个容器,然后采用多线程运行servlet

maven配置

由于servlet不是在jdk标准包里,所以首先要在pom.xml里面添加servlet的包依赖,版本自己选:

1
2
3
4
5
6
7
8
xml复制代码    <dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>

一定要注意的是,scope填privided, 表示在最后打包的时候不将这个依赖放进去,因为在运行的时候都是使用tomcat的自带包。

然后将打包名称改成hello,这样最后会生成hello.war:

1
2
3
xml复制代码    <build>
<finalName>hello</finalName>
</build>

maven打包默认是jar文件,将其修改成war:

1
2
3
4
xml复制代码    <groupId>groupid</groupId>
<artifactId>projectname</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>

前三项都是自己设定的项目信息,只要在最后添加packaging一项就行,然后修改项目的结构,需要在src文件夹建立webapp这个包,其他两项java,resources都是已经存在的。最重要的是web.xml这个文件,这个之后再讲。

1
2
3
4
5
6
7
8
shell复制代码├─java
│ └─test
│ HelloWorld.java
│
├─resources
└─webapp
└─WEB-INF
web.xml

编写servlet

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复制代码package test;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class HelloWorld extends HttpServlet {
private String message; //unsafe
@Override
public void init() throws ServletException {
message = "Hello World";
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");

PrintWriter out = resp.getWriter();
out.println(message);
}

@Override
public void destroy() {
super.destroy();
}
}

配置web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
<servlet>
<servlet-name>Hello</servlet-name>
<servlet-class>test.HelloWorld</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>Hello</servlet-name>
<url-pattern>/Hello</url-pattern>
</servlet-mapping>
</web-app>

前面两项的意思我也不太懂,不过我们一般只需要关注web-app这个标签下的内容,servlet-name可以自己随便指定,指定之后下面的配置都用这个名称来指代目标servlet,上面这个配置是用Hello指代test.HelloWorld这个实现了Servlet接口的类,注意包名带上。下面有一项url-pattern,表示你想让用户使用什么样的地址可以访问到这个servlet。

运行

上面都写好了之后,maven命令clean,compile, package三连,生成了一个hello.war,将这个文件放在tomcat的webapps目录下,然后进入到tomcat的bin文件夹里面,cmd输入

1
shell复制代码startup

然后浏览器输入localhost:8080/hello/Hello就可以看到servlet返回的信息了,其中hello是war文件的名称,后面的/Hello就是url-pattern的值,当然也可以设置成通配符,让其他url也能够连接到这个servlet

可能出现的问题

乱码

tomcat的输出按照目标系统语言,所以有可能乱码,解决方法,将tomcat下conf/logging.properties中java.util.logging.ConsoleHandler.encoding = UTF-8改成= GBK

端口被占用

这时候要修改tomcat的端口,将conf/server.xml里的Connector port=”8080”修改成想要的端口即可。

总结

踩了一天坑

本文转载自: 掘金

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

JimuReport积木报表—表格联动图表专题

发表于 2021-11-23

联动是指在一个报表中点击表格某行或者图表某区域,根据点击数据重新渲染联动的图表。积木报表联动分为表格联动图表和图表联动图表,下面我们就来看看表格数据如何联动图表吧!

# 示例效果

示例:点击班级列表的班级id,联动展示班级下的学生成绩图表。

# 联动配置步骤

1.设计报表

首先准备两个报表,一个数据列表 “班级列表”、一个图形报表 “班级学生分数图表”。
篇幅有限深入请看 入门视频

2.添加数据集

准备上述步骤一,两个报表所需要的数据集

  • “班级列表” — 数据集

查询所有班级信息

  • “班级学生分数图表” — 详情数据集

根据班级 classid 查询学生分数信息

3.重点来了—配置超链接

3.1 添加链接
  • 点击 “班级列表” 中的 “#{ty.id}”单元格,设置右侧面板 “添加链接” +,选择图表联动;如下图:
3.2 联动参数配置
  • 链接名称:填写链接名称(随意填写)
  • 链接图表:选择链接图表—“班级学生分数图表”

参数设置说明:点击“班级列表” 时通过“班级id”来联动“班级学生分数图表”,所以联动参数如下

  • “原始参数—id”为“班级列表” 中的班级id
  • “映射参数—classid”为传至“班级学生分数图表” 中的参数classid

配置完成,保存后,点击预览即可查看联动效果。

附录:

  • 积木报表的联动还支持复杂规则,比如 支持条件,更多配置 请参考文档。
  • 体验官网:www.jimureport.com

本文转载自: 掘金

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

1…216217218…956

开发者博客

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