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

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


  • 首页

  • 归档

  • 搜索

使用 golang 实现 ping 命令 始于珞尘 De

发表于 2018-03-12

ping 是一个经常被用来检查主机间连通性的工具, 它基于 ICMP 协议实现, 基本原理很简单: 本机给远程机器发送 ICMP 报文, 远程主机接收到 ICMP 报文后便会回复一个类似的 ICMP 报文; 当本机接收到回复后变认为远程主机是可连接的, 否则便认为这个主机是不可达的.

为了了解 golang 的网络编程, 我用 go 实现了一个 ping 命令, 本文会介绍如何实现 ping 命令.

Demo

这里有完整的示例代码, 可以直接执行实现下面的效果 (注意需要 sudo 权限):

1
2
3
4
5
6
7
复制代码➜  ping git:(master) sudo go run goping.go baidu.com
Ping 111.13.101.208 (baidu.com):
28 bytes from 111.13.101.208: seq=1 time=9ms
28 bytes from 111.13.101.208: seq=2 time=9ms
28 bytes from 111.13.101.208: seq=3 time=10ms
28 bytes from 111.13.101.208: seq=4 time=10ms
28 bytes from 111.13.101.208: seq=5 time=9ms

如何实现

ICMP 报文

首先我们需要定义出 ICMP 报文头的结构:

1
2
3
4
5
6
复制代码type ICMP struct {
Type uint8
Code uint8
CheckSum uint16
Identifier uint16
SequenceNum uint16

其中 Type 表明的是 ICMP 的类型, Code 则用来进一步划分 ICMP 的类型, ping 使用的是 echo 类型的 ICMP, 这两个值需要分别设置为 8 和 0.

CheckSum 是报文头的校验值, 以防止在网络传输过程中的数据错误. 会先把这个字段设置为 0 来计算校验值, 计算完成后再把校验值赋值到这个字段.

ID 是用来标识一个 ICMP, 可以设置为 0; 而 SequenceNum 则是序列号, 可以在发送 ICMP 报文的时候依次累加.

这篇文章对 ICMP 的结构有更详细的介绍.

基于上面的描述, 我们可以实现下面这个基于序列号生成 ICMP 报文头的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码func getICMP(seq uint16) ICMP {
icmp := ICMP{
Type: 8,
Code: 0,
CheckSum: 0,
Identifier: 0,
SequenceNum: seq,
}
var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, icmp)
icmp.CheckSum = CheckSum(buffer.Bytes())
buffer.Reset()
return icmp
}

其中 CheckSum() 是用来计算校验值的函数. 在网络中传输的数据需要是大端字节序的.

发送及接收 ICMP 报文

首先, 我们使用 net.DialIP("ip4:icmp", nil, destAddr) 来创建一个 ICMP 报文.

接着我们使用下面的代码填充 ICMP 报文并发送:

1
2
3
4
复制代码binary.Write(&buffer, binary.BigEndian, icmp)
if _, err := conn.Write(buffer.Bytes()); err != nil {
return err
}

发送完之后, 我们使用下面的命令接收请求:

1
2
复制代码recv := make([]byte, 1024)
receiveCnt, err := conn.Read(recv)

同时我们还需要统计发送到接收之间所耗费的时间.

完整的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码func sendICMPRequest(icmp ICMP, destAddr *net.IPAddr) error {
conn, err := net.DialIP("ip4:icmp", nil, destAddr)
if err != nil {
fmt.Printf("Fail to connect to remote host: %s\n", err)
return err
}
defer conn.Close()
var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, icmp)
if _, err := conn.Write(buffer.Bytes()); err != nil {
return err
}
tStart := time.Now()
conn.SetReadDeadline((time.Now().Add(time.Second * 2)))
recv := make([]byte, 1024)
receiveCnt, err := conn.Read(recv)
if err != nil {
return err
}
tEnd := time.Now()
duration := tEnd.Sub(tStart).Nanoseconds() / 1e6
fmt.Printf("%d bytes from %s: seq=%d time=%dms\n", receiveCnt, destAddr.String(), icmp.SequenceNum, duration)
return err
}

ping 命令的完整代码

Github 上的文件路径

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
复制代码package main
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"os"
"time"
)
type ICMP struct {
Type uint8
Code uint8
CheckSum uint16
Identifier uint16
SequenceNum uint16
}
func usage() {
msg := `
Need to run as root!
Usage:
goping host
Example: ./goping www.baidu.com`
fmt.Println(msg)
os.Exit(0)
}
func getICMP(seq uint16) ICMP {
icmp := ICMP{
Type: 8,
Code: 0,
CheckSum: 0,
Identifier: 0,
SequenceNum: seq,
}
var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, icmp)
icmp.CheckSum = CheckSum(buffer.Bytes())
buffer.Reset()
return icmp
}
func sendICMPRequest(icmp ICMP, destAddr *net.IPAddr) error {
conn, err := net.DialIP("ip4:icmp", nil, destAddr)
if err != nil {
fmt.Printf("Fail to connect to remote host: %s\n", err)
return err
}
defer conn.Close()
var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, icmp)
if _, err := conn.Write(buffer.Bytes()); err != nil {
return err
}
tStart := time.Now()
conn.SetReadDeadline((time.Now().Add(time.Second * 2)))
recv := make([]byte, 1024)
receiveCnt, err := conn.Read(recv)
if err != nil {
return err
}
tEnd := time.Now()
duration := tEnd.Sub(tStart).Nanoseconds() / 1e6
fmt.Printf("%d bytes from %s: seq=%d time=%dms\n", receiveCnt, destAddr.String(), icmp.SequenceNum, duration)
return err
}
func CheckSum(data []byte) uint16 {
var (
sum uint32
length int = len(data)
index int
)
for length > 1 {
sum += uint32(data[index])<<8 + uint32(data[index+1])
index += 2
length -= 2
}
if length > 0 {
sum += uint32(data[index])
}
sum += (sum >> 16)
return uint16(^sum)
}
func main() {
if len(os.Args) < 2 {
usage()
}
host := os.Args[1]
raddr, err := net.ResolveIPAddr("ip", host)
if err != nil {
fmt.Printf("Fail to resolve %s, %s\n", host, err)
return
}
fmt.Printf("Ping %s (%s):\n\n", raddr.String(), host)
for i := 1; i < 6; i++ {
if err = sendICMPRequest(getICMP(uint16(i)), raddr); err != nil {
fmt.Printf("Error: %s\n", err)
}
time.Sleep(2 * time.Second)
}
}

References

  • Golang实现ping
  • 使用Golang实现简单Ping过程

本文转载自: 掘金

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

Golang cgo 开发小结

发表于 2018-03-12

工作上遇到一个需求,需要把一个C++的动态库的功能封装为Web接口。由于没有C++开发经验,C有点经验,于是考虑了两种方案:

  1. 封装为PHP扩展
  2. 在Golang中使用CGO

两种方案我都可以做,但最终决定采用第2种方案,主要考虑的因素是这个Web服务最终需要在客户那里进行私有化部署,采用PHP的话,部署的时候还需要Nginx、Fpm(当然也可以直接用Swoole),但是PHP代码是明文的,虽然可以买一些商业软件进行加密(比如Swoole Compiler)。如果直接用Golang的话,就可以直接给用户部署一个二进制程序(需要strip掉符号信息)就可以了,部署起来更方便。

下面将通过一个示例程序,演示如何在Golang中通过cgo调用C++。

示例代码目录:

1
2
3
4
5
6
7
8
9
10
复制代码.
├── bin
│ └── cgo
└── src
└── cgo
├── c_src.cpp // 在Golang中调用的C函数定义
├── c_src.h // C头文件,声明了哪些C函数会在Golang中使用,在main.go中包含
├── main.go
├── src.cpp // C++代码
└── src.hpp // C++头文件

c_src.h 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码#ifndef WRAP_CPP_H
#define WRAP_CPP_H

#ifdef __cplusplus
extern "C" {
#endif // __cplusplus

typedef void* Foo;
Foo FooNew();
void FooDestroy(Foo f);
const char* FooGetName(Foo f, int* retLen);
void FooSetName(Foo f, char* name);

#ifdef __cplusplus
}
#endif // __cplusplus

#endif // WRAP_CPP_H

extern "C"作用:Combining C++ and C - how does #ifdef __cplusplus work?

c_src.cpp 源码:

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
复制代码#include "src.hpp"
#include "c_src.h"
#include <cstring>

// 返回cxxFoo对象,但转换为void*
Foo FooNew()
{
cxxFoo* ret = new cxxFoo("rokety");
return (void*)ret;
}

void FooDestroy(Foo f)
{
cxxFoo* foo = (cxxFoo*)f;
delete foo;
}

// 封装cxxFoo的get_name方法
const char* FooGetName(Foo f, int* ret_len)
{
cxxFoo* foo = (cxxFoo*)f;
std::string name = foo->get_name();
*ret_len = name.length();
const char* ret_str = (const char*)malloc(*ret_len);
memcpy((void*)ret_str, name.c_str(), *ret_len);
return ret_str;
}

// 封装cxxFoo的set_name方法
void FooSetName(Foo f, char* name)
{
cxxFoo* foo = (cxxFoo*)f;
std::string _name(name, strlen(name));
foo->set_name(_name);
}

c_src.cpp 可能的疑问:

  • 为何需要定义Foo?因为在C中没有Class的概念,所以需要把C++的Class转换为C中的数据类型
  • 为何在FooGetName中需要进行malloc和memcpy?因为name是局部变量,并且内存分配在栈上,当cgo调用返回后,name所占用的内存会被释放掉。

main.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
复制代码package main

// #include "c_src.h"
// #include <stdlib.h>
import "C"

import (
"fmt"
"unsafe"
)

type GoFoo struct {
foo C.Foo
}

func NewGoFoo() GoFoo {
var ret GoFoo
ret.foo = C.FooNew()
return ret
}

func (f GoFoo) Destroy() {
C.FooDestroy(f.foo)
}

func (f GoFoo) GetName() string {
rLen := C.int(0)
name := C.FooGetName(f.foo, &rLen)
defer C.free(unsafe.Pointer(name)) // 必须使用C的free函数,释放FooGetName中malloc的内存
return C.GoStringN(name, rLen) // 从name构造出golang的string类型值
}

func (f GoFoo) SetName(name string) {
cname := C.CString(name) // 将golang的string类型值转换为c中的char*类型值,这里会调用到c的malloc
C.FooSetName(f.foo, cname)
C.free(unsafe.Pointer(cname)) // 释放上面malloc的内存
}

func main() {
foo := NewGoFoo()
fmt.Println(foo.GetName())
foo.GetName()
foo.SetName("new rokety")
fmt.Println(foo.GetName())
foo.Destroy()
}

main.go 可能的疑问:

  • unsafe.Pointer(…)相当于把变量强转为C中的void*类型
  • SetName中为何需要做转换,因为name变量的内存是在Golang中分配的,且string类型是不可修改的,因此,需要在c中分配name所需要的内存,以便在FooSetName中使用
  • 需要注意的一点是import "C"上面必须紧跟// #include ...注释

src.hpp 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码#ifndef CXX_H
#define CXX_H

#include <string>

class cxxFoo
{
public:
cxxFoo(std::string name);
~cxxFoo();
std::string get_name();
void set_name(std::string name);

private:
std::string name;
};

#endif // CXX_H

src.cpp 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码#include "src.hpp"
#include <iostream>

cxxFoo::cxxFoo(std::string name)
{
this->name = name;
}

cxxFoo::~cxxFoo()
{
}

std::string cxxFoo::get_name()
{
return this->name;
}

void cxxFoo::set_name(std::string name)
{
this->name = name;
}

小结:

  • C中的数据类型会与Golang的C.xxx数据类型对应:CGO 类型(CGO Types)
  • 在C/C++中申请的内存,就得在C/C++中释放
  • 对于需要链接C/C++动态库,或加上编译参数,可以在import "C"加上对应注释// #cgo CFLAGS: -DPNG_DEBUG=1

参考资料:

  • How to use C++ in Go?
  • Command cgo
  • C? Go? Cgo!
  • Golang CGO编程之调用返回char*指针及长度的C函数库
  • CGO: Go与C互操作技术(一):Go调C基本原理

本文转载自: 掘金

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

Java源码分析:HashMap 18 相对于17 到底

发表于 2018-03-12

前言

  • HashMap 在 Java 和 Android 开发中非常常见
  • 而HashMap 1.8 相对于 HashMap 1.7 更新多
  • 今天,我将通过源码分析HashMap 1.8 ,从而讲解HashMap 1.8 相对于 HashMap 1.7 的更新内容,希望你们会喜欢。
  1. 本文基于版本 JDK 1.8,即 Java 8
  2. 关于版本 JDK 1.7,即 Java 7,具体请看文章Java:手把手带你源码分析 HashMap 1.7

目录

示意图


  1. 简介

  • 类定义
1
2
3
复制代码public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
  • 主要简介

示意图

  • HashMap 的实现在 JDK 1.7 和 JDK 1.8 差别较大
  • 今天,我将对照 JDK 1.7的源码,在此基础上讲解 JDK 1.8 中 HashMap 的源码解析

请务必打开JDK 1.7对照看:Java:手把手带你源码分析 HashMap 1.7


  1. 数据结构:引入了 红黑树

2.1 主要介绍

示意图

关于 红黑树 了解:blog.csdn.net/v_july_v/ar…

示意图

2.2 存储流程

注:为了让大家有个感性的认识,只是简单的画出存储流程,更加详细 & 具体的存储流程会在下面源码分析中给出

示意图

2.3 数组元素 & 链表节点的 实现类

  • HashMap中的数组元素 & 链表节点 采用 Node类 实现

与 JDK 1.7 的对比(Entry类),仅仅只是换了名字

  • 该类的源码分析如下

具体分析请看注释

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
复制代码/** 
* Node = HashMap的内部类,实现了Map.Entry接口,本质是 = 一个映射(键值对)
* 实现了getKey()、getValue()、equals(Object o)和hashCode()等方法
**/

static class Node<K,V> implements Map.Entry<K,V> {

final int hash; // 哈希值,HashMap根据该值确定记录的位置
final K key; // key
V value; // value
Node<K,V> next;// 链表下一个节点

// 构造方法
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

public final K getKey() { return key; } // 返回 与 此项 对应的键
public final V getValue() { return value; } // 返回 与 此项 对应的值
public final String toString() { return key + "=" + value; }

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

/**
* hashCode()
*/
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

/**
* equals()
* 作用:判断2个Entry是否相等,必须key和value都相等,才返回true
*/
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}

2.4 红黑树节点 实现类

  • HashMap中的红黑树节点 采用 TreeNode 类 实现
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
复制代码 /**
* 红黑树节点 实现类:继承自LinkedHashMap.Entry<K,V>类
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {

// 属性 = 父节点、左子树、右子树、删除辅助节点 + 颜色
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
boolean red;

// 构造函数
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}

// 返回当前节点的根节点
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}

  1. 具体使用

3.1 主要使用API(方法、函数)

与 JDK 1.7 基本相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码V get(Object key); // 获得指定键的值
V put(K key, V value); // 添加键值对
void putAll(Map<? extends K, ? extends V> m); // 将指定Map中的键值对 复制到 此Map中
V remove(Object key); // 删除该键值对

boolean containsKey(Object key); // 判断是否存在该键的键值对;是 则返回true
boolean containsValue(Object value); // 判断是否存在该值的键值对;是 则返回true

Set<K> keySet(); // 单独抽取key序列,将所有key生成一个Set
Collection<V> values(); // 单独value序列,将所有value生成一个Collection

void clear(); // 清除哈希表中的所有键值对
int size(); // 返回哈希表中所有 键值对的数量 = 数组中的键值对 + 链表中的键值对
boolean isEmpty(); // 判断HashMap是否为空;size == 0时 表示为 空

3.2 使用流程

与 JDK 1.7 基本相同

  • 在具体使用时,主要流程是:
  1. 声明1个 HashMap的对象
  2. 向 HashMap 添加数据(成对 放入 键 - 值对)
  3. 获取 HashMap 的某个数据
  4. 获取 HashMap 的全部数据:遍历HashMap
  • 示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
复制代码import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class HashMapTest {

public static void main(String[] args) {
/**
* 1. 声明1个 HashMap的对象
*/
Map<String, Integer> map = new HashMap<String, Integer>();

/**
* 2. 向HashMap添加数据(成对 放入 键 - 值对)
*/
map.put("Android", 1);
map.put("Java", 2);
map.put("iOS", 3);
map.put("数据挖掘", 4);
map.put("产品经理", 5);

/**
* 3. 获取 HashMap 的某个数据
*/
System.out.println("key = 产品经理时的值为:" + map.get("产品经理"));

/**
* 4. 获取 HashMap 的全部数据:遍历HashMap
* 核心思想:
* 步骤1:获得key-value对(Entry) 或 key 或 value的Set集合
* 步骤2:遍历上述Set集合(使用for循环 、 迭代器(Iterator)均可)
* 方法共有3种:分别针对 key-value对(Entry) 或 key 或 value
*/

// 方法1:获得key-value的Set集合 再遍历
System.out.println("方法1");
// 1. 获得key-value对(Entry)的Set集合
Set<Map.Entry<String, Integer>> entrySet = map.entrySet();

// 2. 遍历Set集合,从而获取key-value
// 2.1 通过for循环
for(Map.Entry<String, Integer> entry : entrySet){
System.out.print(entry.getKey());
System.out.println(entry.getValue());
}
System.out.println("----------");
// 2.2 通过迭代器:先获得key-value对(Entry)的Iterator,再循环遍历
Iterator iter1 = entrySet.iterator();
while (iter1.hasNext()) {
// 遍历时,需先获取entry,再分别获取key、value
Map.Entry entry = (Map.Entry) iter1.next();
System.out.print((String) entry.getKey());
System.out.println((Integer) entry.getValue());
}


// 方法2:获得key的Set集合 再遍历
System.out.println("方法2");

// 1. 获得key的Set集合
Set<String> keySet = map.keySet();

// 2. 遍历Set集合,从而获取key,再获取value
// 2.1 通过for循环
for(String key : keySet){
System.out.print(key);
System.out.println(map.get(key));
}

System.out.println("----------");

// 2.2 通过迭代器:先获得key的Iterator,再循环遍历
Iterator iter2 = keySet.iterator();
String key = null;
while (iter2.hasNext()) {
key = (String)iter2.next();
System.out.print(key);
System.out.println(map.get(key));
}


// 方法3:获得value的Set集合 再遍历
System.out.println("方法3");

// 1. 获得value的Set集合
Collection valueSet = map.values();

// 2. 遍历Set集合,从而获取value
// 2.1 获得values 的Iterator
Iterator iter3 = valueSet.iterator();
// 2.2 通过遍历,直接获取value
while (iter3.hasNext()) {
System.out.println(iter3.next());
}

}


}

// 注:对于遍历方式,推荐使用针对 key-value对(Entry)的方式:效率高
// 原因:
// 1. 对于 遍历keySet 、valueSet,实质上 = 遍历了2次:1 = 转为 iterator 迭代器遍历、2 = 从 HashMap 中取出 key 的 value 操作(通过 key 值 hashCode 和 equals 索引)
// 2. 对于 遍历 entrySet ,实质 = 遍历了1次 = 获取存储实体Entry(存储了key 和 value )
  • 运行结果
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
复制代码方法1
Java2
iOS3
数据挖掘4
Android1
产品经理5
----------
Java2
iOS3
数据挖掘4
Android1
产品经理5
方法2
Java2
iOS3
数据挖掘4
Android1
产品经理5
----------
Java2
iOS3
数据挖掘4
Android1
产品经理5
方法3
2
3
4
1
5

下面,我们按照上述的使用过程,对一个个步骤进行源码解析


  1. 基础知识:HashMap中的重要参数(变量)

  • 在进行真正的源码分析前,先讲解HashMap中的重要参数(变量)
  • HashMap中的主要参数 同 JDK 1.7 ,即:容量、加载因子、扩容阈值
  • 但由于数据结构中引入了 红黑树,故加入了 与红黑树相关的参数。具体介绍如下:
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
复制代码 /** 
* 主要参数 同 JDK 1.7
* 即:容量、加载因子、扩容阈值(要求、范围均相同)
*/

// 1. 容量(capacity): 必须是2的幂 & <最大容量(2的30次方)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的2^4=16
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量 = 2的30次方(若传入的容量过大,将被最大值替换)

// 2. 加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度
final float loadFactor; // 实际加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子 = 0.75

// 3. 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)
// a. 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数
// b. 扩容阈值 = 容量 x 加载因子
int threshold;

// 4. 其他
transient Node<K,V>[] table; // 存储数据的Node类型 数组,长度 = 2的幂;数组的每个元素 = 1个单链表
transient int size;// HashMap的大小,即 HashMap中存储的键值对的数量


/**
* 与红黑树相关的参数
*/
// 1. 桶的树化阈值:即 链表转成红黑树的阈值,在存储数据时,当链表长度 > 该值时,则将链表转换成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 2. 桶的链表还原阈值:即 红黑树转为链表的阈值,当在扩容(resize())时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将 红黑树转换成链表
static final int UNTREEIFY_THRESHOLD = 6;
// 3. 最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树)
// 否则,若桶内元素太多时,则直接扩容,而不是树形化
// 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
  • 此处 再次详细说明 加载因子

同 JDK 1.7,但由于其重要性,故此处再次说明

示意图

  • 总结 数据结构 & 参数方面与 JDK 1.7的区别

示意图


  1. 源码分析

  • 本次的源码分析主要是根据 使用步骤 进行相关函数的详细分析
  • 主要分析内容如下:

示意图

  • 下面,我将对每个步骤内容的主要方法进行详细分析

步骤1:声明1个 HashMap的对象

此处主要分析的构造函数 类似 JDK 1.7

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
复制代码/**
* 函数使用原型
*/
Map<String,Integer> map = new HashMap<String,Integer>();

/**
* 源码分析:主要是HashMap的构造函数 = 4个
* 仅贴出关于HashMap构造函数的源码
*/

public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable{

// 省略上节阐述的参数

/**
* 构造函数1:默认构造函数(无参)
* 加载因子 & 容量 = 默认 = 0.75、16
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}

/**
* 构造函数2:指定“容量大小”的构造函数
* 加载因子 = 默认 = 0.75 、容量 = 指定大小
*/
public HashMap(int initialCapacity) {
// 实际上是调用指定“容量大小”和“加载因子”的构造函数
// 只是在传入的加载因子参数 = 默认加载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

/**
* 构造函数3:指定“容量大小”和“加载因子”的构造函数
* 加载因子 & 容量 = 自己指定
*/
public HashMap(int initialCapacity, float loadFactor) {

// 指定初始容量必须非负,否则报错
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);

// HashMap的最大容量只能是MAXIMUM_CAPACITY,哪怕传入的 > 最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;

// 填充比必须为正
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 设置 加载因子
this.loadFactor = loadFactor;

// 设置 扩容阈值
// 注:此处不是真正的阈值,仅仅只是将传入的容量大小转化为:>传入容量大小的最小的2的幂,该阈值后面会重新计算
// 下面会详细讲解 ->> 分析1
this.threshold = tableSizeFor(initialCapacity);

}

/**
* 构造函数4:包含“子Map”的构造函数
* 即 构造出来的HashMap包含传入Map的映射关系
* 加载因子 & 容量 = 默认
*/

public HashMap(Map<? extends K, ? extends V> m) {

// 设置容量大小 & 加载因子 = 默认
this.loadFactor = DEFAULT_LOAD_FACTOR;

// 将传入的子Map中的全部元素逐个添加到HashMap中
putMapEntries(m, false);
}
}

/**
* 分析1:tableSizeFor(initialCapacity)
* 作用:将传入的容量大小转化为:>传入容量大小的最小的2的幂
* 与JDK 1.7对比:类似于JDK 1.7 中 inflateTable()里的 roundUpToPowerOf2(toSize)
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
  • 注:(同JDK 1.7类似)
    1. 此处仅用于接收初始容量大小(capacity)、加载因子(Load factor),但仍无真正初始化哈希表,即初始化存储数组table
    2. 此处先给出结论:真正初始化哈希表(初始化存储数组table)是在第1次添加键值对时,即第1次调用put()时。下面会详细说明

至此,关于HashMap的构造函数讲解完毕。


步骤2:向HashMap添加数据(成对 放入 键 - 值对)

  • 在该步骤中,与JDK 1.7的差别较大:

示意图

下面会对上述区别进行详细讲解

  • 添加数据的流程如下

注:为了让大家有个感性的认识,只是简单的画出存储流程,更加详细 & 具体的存储流程会在下面源码分析中给出

示意图

  • 源码分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码 /**
* 函数使用原型
*/
map.put("Android", 1);
map.put("Java", 2);
map.put("iOS", 3);
map.put("数据挖掘", 4);
map.put("产品经理", 5);

/**
* 源码分析:主要分析HashMap的put函数
*/
public V put(K key, V value) {
// 1. 对传入数组的键Key计算Hash值 ->>分析1
// 2. 再调用putVal()添加数据进去 ->>分析2
return putVal(hash(key), key, value, false, true);
}

下面,将详细讲解 上面的2个主要分析点

分析1:hash(key)

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
复制代码   /**
* 分析1:hash(key)
* 作用:计算传入数据的哈希码(哈希值、Hash值)
* 该函数在JDK 1.7 和 1.8 中的实现不同,但原理一样 = 扰动函数 = 使得根据key生成的哈希码(hash值)分布更加均匀、更具备随机性,避免出现hash值冲突(即指不同key但生成同1个hash值)
* JDK 1.7 做了9次扰动处理 = 4次位运算 + 5次异或运算
* JDK 1.8 简化了扰动函数 = 只做了2次扰动 = 1次位运算 + 1次异或运算
*/

// JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
static final int hash(int h) {
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

// JDK 1.8实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动)
// 1. 取hashCode值: h = key.hashCode()
// 2. 高位参与低位的运算:h ^ (h >>> 16)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// a. 当key = null时,hash值 = 0,所以HashMap的key 可为null
// 注:对比HashTable,HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
// b. 当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),然后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制
}

/**
* 计算存储位置的函数分析:indexFor(hash, table.length)
* 注:该函数仅存在于JDK 1.7 ,JDK 1.8中实际上无该函数(直接用1条语句判断写出),但原理相同
* 为了方便讲解,故提前到此讲解
*/
static int indexFor(int h, int length) {
return h & (length-1);
// 将对哈希码扰动处理后的结果 与运算(&) (数组长度-1),最终得到存储在数组table的位置(即数组下标、索引)
}
  • 总结 计算存放在数组 table 中的位置(即数组下标、索引)的过程
  1. 此处与 JDK 1.7的区别在于:hash值的求解过程中 哈希码的二次处理方式(扰动处理)
  2. 步骤1、2 = hash值的求解过程

示意图

  • 计算示意图

示意图


在了解 如何计算存放数组table 中的位置 后,所谓 知其然 而 需知其所以然,下面我将讲解为什么要这样计算,即主要解答以下3个问题:

  1. 为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?
  2. 为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?
  3. 为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?

在回答这3个问题前,请大家记住一个核心思想:

所有处理的根本目的,都是为了提高 存储key-value的数组下标位置 的随机性 & 分布均匀性,尽量避免出现hash值冲突。即:对于不同key,存储的数组下标位置要尽可能不一样

问题1:为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?

  • 结论:容易出现 哈希码 与 数组大小范围不匹配的情况,即 计算出来的哈希码可能 不在数组大小范围内,从而导致无法匹配存储位置
  • 原因描述

示意图

  • 为了解决 “哈希码与数组大小范围不匹配” 的问题,HashMap给出了解决方案:哈希码 与运算(&) (数组长度-1),即问题3

问题2:为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?

  • 结论:根据HashMap的容量大小(数组长度),按需取 哈希码一定数量的低位 作为存储的数组下标位置,从而 解决 “哈希码与数组大小范围不匹配” 的问题
  • 具体解决方案描述

示意图

问题3:为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?

  • 结论:加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突
  • 具体描述

示意图

至此,关于怎么计算 key-value 值存储在HashMap数组位置 & 为什么要这么计算,讲解完毕。


分析2:putVal(hash(key), key, value, false, true);

此处有2个主要讲解点:

  • 计算完存储位置后,具体该如何 存放数据 到哈希表中
  • 具体如何扩容,即 扩容机制

主要讲解点1:计算完存储位置后,具体该如何存放数据到哈希表中

由于数据结构中加入了红黑树,所以在存放数据到哈希表中时,需进行多次数据结构的判断:数组、红黑树、链表

与 JDK 1.7的区别: JDK 1.7只需判断 数组 & 链表

示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
复制代码   /**
* 分析2:putVal(hash(key), key, value, false, true)
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {

Node<K,V>[] tab; Node<K,V> p; int n, i;

// 1. 若哈希表的数组tab为空,则 通过resize() 创建
// 所以,初始化哈希表的时机 = 第1次调用put函数时,即调用resize() 初始化创建
// 关于resize()的源码分析将在下面讲解扩容时详细分析,此处先跳过
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;

// 2. 计算插入存储的数组索引i:根据键值key计算的hash值 得到
// 此处的数组下标计算方式 = i = (n - 1) & hash,同JDK 1.7中的indexFor(),上面已详细描述

// 3. 插入时,需判断是否存在Hash冲突:
// 若不存在(即当前table[i] == null),则直接在该数组位置新建节点,插入完毕
// 否则,代表存在Hash冲突,即当前存储位置已存在节点,则依次往下判断:a. 当前位置的key是否与需插入的key相同、b. 判断需插入的数据结构是否为红黑树 or 链表
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // newNode(hash, key, value, null)的源码 = new Node<>(hash, key, value, next)

else {
Node<K,V> e; K k;

// a. 判断 table[i]的元素的key是否与 需插入的key一样,若相同则 直接用新value 覆盖 旧value
// 判断原则:equals()
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;

// b. 继续判断:需插入的数据结构是否为红黑树 or 链表
// 若是红黑树,则直接在树中插入 or 更新键值对
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); ->>分析3

// 若是链表,则在链表中插入 or 更新键值对
// i. 遍历table[i],判断Key是否已存在:采用equals() 对比当前遍历节点的key 与 需插入数据的key:若已存在,则直接用新value 覆盖 旧value
// ii. 遍历完毕后仍无发现上述情况,则直接在链表尾部插入数据
// 注:新增节点后,需判断链表长度是否>8(8 = 桶的树化阈值):若是,则把链表转换为红黑树

else {
for (int binCount = 0; ; ++binCount) {
// 对于ii:若数组的下1个位置,表示已到表尾也没有找到key值相同节点,则新建节点 = 插入节点
// 注:此处是从链表尾插入,与JDK 1.7不同(从链表头插入,即永远都是添加到数组的位置,原来数组位置的数据则往后移)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);

// 插入节点后,若链表节点>数阈值,则将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash); // 树化操作
break;
}

// 对于i
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;

// 更新p指向下一个节点,继续遍历
p = e;
}
}

// 对i情况的后续操作:发现key已存在,直接用新value 覆盖 旧value & 返回旧value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 替换旧值时会调用的方法(默认实现为空)
return oldValue;
}
}

++modCount;

// 插入成功后,判断实际存在的键值对数量size > 最大容量threshold
// 若 > ,则进行扩容 ->>分析4(但单独讲解,请直接跳出该代码块)
if (++size > threshold)
resize();

afterNodeInsertion(evict);// 插入成功时会调用的方法(默认实现为空)
return null;

}

/**
* 分析3:putTreeVal(this, tab, hash, key, value)
* 作用:向红黑树插入 or 更新数据(键值对)
* 过程:遍历红黑树判断该节点的key是否与需插入的key 相同:
* a. 若相同,则新value覆盖旧value
* b. 若不相同,则插入
*/

final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}

TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
  • 总结

示意图

主要讲解点2:扩容机制(即 resize()函数方法)

  • 扩容流程如下

示意图

  • 源码分析
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
复制代码   /**
* 分析4:resize()
* 该函数有2种使用情况:1.初始化哈希表 2.当前数组容量过小,需扩容
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 扩容前的数组(当前数组)
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 扩容前的数组的容量 = 长度
int oldThr = threshold;// 扩容前的数组的阈值
int newCap, newThr = 0;

// 针对情况2:若扩容前的数组容量超过最大值,则不再扩充
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}

// 针对情况2:若无超过最大值,就扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 通过右移扩充2倍
}

// 针对情况1:初始化哈希表(采用指定 or 默认值)
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;

else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}

// 计算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}

threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;

if (oldTab != null) {
// 把每个bucket都移动到新的buckets中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;

if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

else { // 链表优化重hash的代码块
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 原索引
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引 + oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
  • 扩容流程(含 与 JDK 1.7 的对比)

示意图

此处主要讲解: JDK 1.8扩容时,数据存储位置重新计算的方式

  • 计算结论 & 原因解析

示意图

  • 结论示意图

示意图

  • 数组位置转换的示意图

示意图

  • JDK 1.8根据此结论作出的新元素存储位置计算规则 非常简单,提高了扩容效率,具体如下图

这与 JDK 1.7在计算新元素的存储位置有很大区别:JDK 1.7在扩容后,都需按照原来方法重新计算,即
hashCode()->> 扰动处理 ->>(h & length-1))

总结

  • 添加数据的流程

示意图

  • 与 JDK 1.7的区别

示意图

至此,关于 HashMap的添加数据源码分析 分析完毕。


步骤3:从HashMap中获取数据

  • 假如理解了上述put()函数的原理,那么get()函数非常好理解,因为二者的过程原理几乎相同
  • get()函数的流程如下:

示意图

  • 源码分析
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
复制代码/**
* 函数原型
* 作用:根据键key,向HashMap获取对应的值
*/
map.get(key);


/**
* 源码分析
*/
public V get(Object key) {
Node<K,V> e;
// 1. 计算需获取数据的hash值
// 2. 通过getNode()获取所查询的数据 ->>分析1
// 3. 获取后,判断数据是否为空
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
* 分析1:getNode(hash(key), key))
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

// 1. 计算存放在数组table中的位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {

// 4. 通过该函数,依次在数组、红黑树、链表中查找(通过equals()判断)
// a. 先在数组中找,若存在,则直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;

// b. 若数组中没有,则到红黑树中寻找
if ((e = first.next) != null) {
// 在树中get
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);

// c. 若红黑树中也没有,则通过遍历,到链表中寻找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

至此,关于 “向 HashMap 获取数据 “讲解完毕。


步骤4:对HashMap的其他操作

即 对其余使用API(函数、方法)的源码分析

  • HashMap除了核心的put()、get()函数,还有以下主要使用的函数方法
1
2
3
4
5
6
7
8
9
10
复制代码
void clear(); // 清除哈希表中的所有键值对
int size(); // 返回哈希表中所有 键值对的数量 = 数组中的键值对 + 链表中的键值对
boolean isEmpty(); // 判断HashMap是否为空;size == 0时 表示为 空

void putAll(Map<? extends K, ? extends V> m); // 将指定Map中的键值对 复制到 此Map中
V remove(Object key); // 删除该键值对

boolean containsKey(Object key); // 判断是否存在该键的键值对;是 则返回true
boolean containsValue(Object value); // 判断是否存在该值的键值对;是 则返回true
  • 关于上述方法的源码的原理 同 JDK 1.7,此处不作过多描述

感兴趣的同学可以参考文章 第5小节 进行类比。

至此,关于 HashMap的底层原理 & 主要使用API(函数、方法)讲解完毕。


  1. 源码总结

下面,用3个图总结整个源码内容:

总结内容 = 数据结构、主要参数、添加 & 查询数据流程、扩容机制

  • 数据结构 & 主要参数

示意图

  • 添加 & 查询数据流程

示意图

  • 扩容机制

示意图


  1. 与 JDK 1.7 的区别

HashMap 的实现在 JDK 1.7 和 JDK 1.8 差别较大,具体区别如下

  1. JDK 1.8 的优化目的主要是:减少 Hash冲突 & 提高哈希表的存、取效率
  2. 关于 JDK 1.7 中 HashMap 的源码解析请看文章:Java:手把手带你源码分析 HashMap 1.7

7.1 数据结构

示意图

7.2 获取数据时(获取数据 类似)

示意图

7.3 扩容机制

示意图


  1. 额外补充:关于HashMap的其他问题

  • 有几个小问题需要在此补充

示意图

  • 具体如下

8.1 哈希表如何解决Hash冲突

示意图

8.2 为什么HashMap具备下述特点:键-值(key-value)都允许为空、线程不安全、不保证有序、存储位置随时间变化

  • 具体解答如下

示意图

  • 下面主要讲解 HashMap 线程不安全的其中一个重要原因:多线程下容易出现resize()死循环
  • 本质 = 并发 执行 put()操作导致触发 扩容行为,从而导致 环形链表,使得在获取数据遍历链表时形成死循环,即Infinite Loop*
  • 先看扩容的源码分析resize()

关于resize()的源码分析已在上文详细分析,此处仅作重点分析:transfer()

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
复制代码/**
* 源码分析:resize(2 * table.length)
* 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍)
*/
void resize(int newCapacity) {

// 1. 保存旧数组(old table)
Entry[] oldTable = table;

// 2. 保存旧容量(old capacity ),即数组长度
int oldCapacity = oldTable.length;

// 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}

// 4. 根据新容量(2倍容量)新建1个数组,即新table
Entry[] newTable = new Entry[newCapacity];

// 5. (重点分析)将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1
transfer(newTable);

// 6. 新数组table引用到HashMap的table属性上
table = newTable;

// 7. 重新设置阈值
threshold = (int)(newCapacity * loadFactor);
}

/**
* 分析1.1:transfer(newTable);
* 作用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容
* 过程:按旧链表的正序遍历链表、在新链表的头部依次插入
*/
void transfer(Entry[] newTable) {
// 1. src引用了旧数组
Entry[] src = table;

// 2. 获取新数组的大小 = 获取新容量大小
int newCapacity = newTable.length;

// 3. 通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中
for (int j = 0; j < src.length; j++) {
// 3.1 取得旧数组的每个元素
Entry<K,V> e = src[j];
if (e != null) {
// 3.2 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象)
src[j] = null;

do {
// 3.3 遍历 以该数组元素为首 的链表
// 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开
Entry<K,V> next = e.next;
// 3.3 重新计算每个元素的存储位置
int i = indexFor(e.hash, newCapacity);
// 3.4 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中
// 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入
e.next = newTable[i];
newTable[i] = e;
// 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点
e = next;
} while (e != null);
// 如此不断循环,直到遍历完数组上的所有数据元素
}
}
}

从上面可看出:在扩容resize()过程中,在将旧数组上的数据 转移到 新数组上时,转移数据操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况

设重新计算存储位置后不变,即扩容前 = 1->2->3,扩容后 = 3->2->1

  • 此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态,具体请看下图:

初始状态、步骤1、步骤2

示意图

示意图

示意图

注:由于 JDK 1.8 转移数据操作 = 按旧链表的正序遍历链表、在新链表的尾部依次插入,所以不会出现链表 逆序、倒置的情况,故不容易出现环形链表的情况。

但 JDK 1.8 还是线程不安全,因为 无加同步锁保护

8.3 为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键

示意图

8.4 HashMap 中的 key若 Object类型, 则需实现哪些方法?

示意图

至此,关于HashMap的所有知识讲解完毕。


  1. 总结

  • 本文主要讲解 Java的 HashMap源码 & 相关知识
  • 下面我将继续对Java、 Android中的其他知识 深入讲解 ,有兴趣可以继续关注Carson_Ho的安卓开发笔记

请帮顶!因为你的鼓励是我写作的最大动力!


欢迎关注carson_ho的微信公众号

示意图

示意图

本文转载自: 掘金

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

抱歉!不要用Java的语法思维来写Kotlin 我全科-

发表于 2018-03-08

写了多年的Java,直到看到Kotlin,原来代码可以如此优雅!

如果你是像我一样是一名优秀的Java开发者^_^,而且已经想用kotlin来实现你的程序,那么,抱歉!不要用Java的语法思维来写Kotlin,不要让kotlin的优雅埋没。如果你没有Java开发经验,下面的内容也对你会有帮助。。。

1.尽可能的少用 !!

个人感觉对于Null的检查是Koltin最语法糖的东西了,强制在编码过程中考虑空指针,因此《十亿美元的错误》,也许你不会再有这个机会犯错了(也许可以说成,你赚了十亿美金^_^)。

首先需要介绍是!!操作符。

!! 操作符:这是为空指针爱好者准备的,非空断言运算符(!!)将任何值转换为非空类型,若该值为空则抛出异常。我们可以写 a!! ,这会返回一个非空的 a 值 (例如:在我们例子中的 String)或者如果 a 为空,就会抛出一个 空指针 异常:

1
复制代码val b = a!!.length

所以,我们能不用!!操作符就不要用。。。

下面介绍几种方式避免使用!!操作符

1).多用 val 而不是 var

在 Kotlin 中 val代表只读,var代表可变。建议尽可能多的使用val。val是线程安全的,并且必须在定义时初始化,所以不需要担心 null 的问题。只需要注意 val 在某些情况下也是可变的就行了。

val 和 var 是用于表示属性是否有 getter/setter:

  • var:同时有 getter 和 setter
  • val:只有 getter

所以,强烈推荐能用val的地方就用val。

2).使用 lateinit

有些时候并不能用val,比如在spring boot接口测试中就不能使用val,对于这种情况,可以使用 lateinit 关键字。。

依赖倒转,对象的创建是通过spring完成的,而val要求定义的时候初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码/**
* Created by quanke on 2018/1/9.
* Site:http://woquanke.com .
*/
@RunWith(SpringRunner::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ApplicationTests {
val log = LogFactory.getLog(ApplicationTests::class.java)!!
@Autowired
lateinit var restTemplate: TestRestTemplate

@Test
fun `GET when given quanke then returns "Hello, quanke"`() {
// Given
val name = "quanke"
// When
val body = restTemplate.getForObject("/users/hello/{name}", String::class.java, name)
// Then
assertThat(body).isEqualTo("Hello, $name")
}
}

注意:lateinit很好用,但也有坑

  • 访问未初始化的 lateinit 属性会导致 UninitializedPropertyAccessException。
  • lateinit 不支持基础数据类型,比如 Int。对于基础数据类型,我们可以这样:
1
2
3
4
> 复制代码> private var mNumber: Int by Delegates.notNull<Int>()
> >
>
>

3).Elvis 操作符

当b为可空引用时,我们可以使用if表达式处理

1
复制代码val l: Int = if (b != null) b.length else -1

但更加优雅的方式是使用Elvis 操作符?:

1
复制代码val l = b?.length ?: -1

如果 ?: 左侧表达式非空,elvis 操作符就返回其左侧表达式,否则返回右侧表达式。

注意:当且仅当左侧为空时,才会对右侧表达式求值。

4).也许可以尝试一下let函数

let函数一般与安全调用操作符一起使用,我们首先介绍安全调用操作?.

1
复制代码b?.length

如果 b 非空,就返回 b.length,否则返回 null,这个表达式的类型是 Int?。

安全调用在链式调用中很有用。例如,如果一个员工Quanke可能会(或者不会)分配给一个部门, 并且可能有另外一个员工是该部门的负责人,那么获取 Quanke 所在部门负责人(如果有的话)的名字,我们写作:

1
复制代码quanke?.department?.head?.name

如果任意一个属性(环节)为空,这个链式调用就会返回 null。

如果要只对非空值执行某个操作,安全调用操作符可以与 let 一起使用:

1
2
3
4
复制代码val listWithNulls: List<String?> = listOf("A", null)
for (item in listWithNulls) {
item?.let { println(it) } // 输出 A 并忽略 null
}

还有一种常见的错误(放ide里面试试就知道什么错误了):

1
2
3
4
5
6
复制代码private var a: String? = null
fun aLetDemo() {
if (a != null) {
test(a)
}
}

我们可以这样:

1
2
3
4
5
6
复制代码private var a: String? = null
fun aLetDemo() {
if (a != null) {
test(a!!)
}
}

但是这样的后果就是你还是需要在test函数里处理空指针。

我们充分利用?.加let的特点,更加优雅的解决这个编译错误,如下

1
2
3
4
5
6
7
8
复制代码private var a: String? = null
fun aLetDemo() {
if (a != null) {
a?.let {
test(it)
}
}
}

2.少写点Util类和继承

很多时候框架提供给我的方法是比较原子,或者某些常用的方法框架并没有提供,Java一般是写一个工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public final class StringUtil {
/**
* 删除所有的标点符号
*
* @param str 处理的字符串
*/
public static String trimPunct(String str) {
if(isEmpty(str)){
return "";
}
return str.replaceAll("[\\pP\\p{Punct}]", "");
}
}

Kotlin可以通过扩展函数的形式实现:

1
2
3
4
5
6
7
8
9
10
复制代码/**
* 删除所有的标点符号
*
* @param str 处理的字符串
*/
fun String.trimPunct(): String {
return if (this.isEmpty()) {
""
} else this.replace("[\\pP\\p{Punct}]".toRegex(), "")
}

调用:

1
2
3
4
复制代码fun main(args: Array<String>) {
val a = "把我的标点符号去掉吧,全科。"
print(a.trimPunct())
}

打印:

1
2
复制代码把我的标点符号去掉吧全科
Process finished with exit code 0

3.别再用+号拼接字符串

无论是Java还是Android开发,我们都会用到字符串拼接,比如进行日志输出等等。在Kotlin中,支持字符串模板,我们可以很轻松的完成一个字符串数的拼接,当然你可能会说使用StringBuilder性能更好,比如:

1
2
3
4
5
复制代码val site = "http://woquanke.com"
val sb: StringBuilder = StringBuilder()
sb.append("我的博客名字叫《我全科》,我的博客地址是:")
sb.append(site)
println(sb.toString())

但kotlin的字符串模版可以优雅的做这个事情:

1
2
复制代码val site = "http://woquanke.com"
println("我的博客名字叫《我全科》,我的博客地址是:$site")

4.也许可以忘记getters/setters了

我们经常创建一些只保存数据的类。在这些类中,一些标准函数往往是操作一下ide生成的。在 Kotlin 中,这叫做 数据类 并标记为 data:

1
复制代码data class User(val name: String, val age: Int)

data class 自动生成getter,setting,hashcode和equals等方法

5.请忘记三元运算符

在 Kotlin 中,if是一个表达式,即它会返回一个值。因此就不需要三元运算符(条件 ? 然后 : 否则),因为普通的 if 就能胜任这个角色。

1
2
复制代码// 作为表达式
val max = if (a > b) a else b

6.哪里还有switch

when 取代了类java 语言的 switch 操作符。其最简单的形式如下:

1
2
3
4
5
6
7
复制代码when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> { // 注意这个块
print("x is neither 1 nor 2")
}
}

如果很多分支需要用相同的方式处理,则可以把多个分支条件放在一起,用逗号分隔:

1
2
3
4
复制代码when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}

可以用任意表达式(而不只是常量)作为分支条件

1
2
3
4
复制代码when (x) {
parseInt(s) -> print("s encodes x")
else -> print("s does not encode x")
}

也可以检测一个值在(in)或者不在(!in)一个区间或者集合中:

1
2
3
4
5
6
复制代码when (x) {
in 1..10 -> print("x is in the range")
in validNumbers -> print("x is valid")
!in 10..20 -> print("x is outside the range")
else -> print("none of the above")
}

另一种可能性是检测一个值是(is)或者不是(!is)一个特定类型的值。注意: 由于智能转换,你可以访问该类型的方法和属性而无需任何额外的检测。

1
2
3
4
复制代码fun hasPrefix(x: Any) = when(x) {
is String -> x.startsWith("prefix")
else -> false
}

when 也可以用来取代 if-else if链。 如果不提供参数,所有的分支条件都是简单的布尔表达式,而当一个分支的条件为真时则执行该分支:

1
2
3
4
5
复制代码when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}

7.去你的ClassCastException

Kotlin智能转换(Smart Casts)

对于不可变的值,Kotlin一般不需要显式转换对象类型,编译器能跟踪is检查类型,在需要时会自动插入类型转换代码(安全):

1
2
3
4
5
复制代码fun classCast(a: Any) {
if (a is String) {
print(a.length) //编译器自动把a转换为String类型
}
}

Kotlin编译器很聪明,能识别反向检查类型!is操作符,会自动插入类型转换代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码if (a !is String) return
print(a.length) //编译器自动把x转换为String类型:
// ||右侧, a自动转换为String类型
if (a !is String || a.length == 0) return
// &&右侧, a自动转换为String类型
if (a is String && a.length > 0) {
print(a.length) // a 自动转换为字符串
}
//智能转换(smart casts)也用于when表达式和while循环
when (a) {
is Int -> print(a + 1)
is String -> print(a.length + 1)
is IntArray -> print(a.sum())
}

如果不能保证变量在类型检查is/!is操作符和变量使用之间不可改变时,智能转换不能用。智能转换的适用条件或规则:

  • val局部变量-总是适用!
  • val属性-适用于private或internal,或者类型检查is/!is在声明属性的同一模块中执行;
  • 不适用于open的属性,或者具有自定义getter的属性!
  • var局部变量—适用于变量在类型检查和使用之间没有修改,且不在修改它的lambda中捕获!
  • var属性-不适用(因为该变量可随时被修改)

安全(可空)转换-操作符as?

为避免抛出异常,可用安全转换操作符as?,在失败时返回null

1
复制代码val a: String? = b as? String

尽管as?右边是一个非空类型String,但是as?转换失败时返回可空(null),换句话说就是,as?函数参数String不能为null,但是as?函数的返回值可以是null

8.真的要习惯Koltin的for循环,太强大了

Kotlin没有Java中的for(初始值;条件;增减步长)这个规则。但是Kotlin中对于for循环语句新增了其他的规则,来满足刚提到的规则。

  • for循环提供迭代器用来遍历任何东西
  • for循环数组被编译为一个基于索引的循环,它不会创建一个迭代器对象

新增的规则,去满足for(初始值;条件;增减步长)这个规则

递增

关键字:until
范围:until[n,m) => 即大于等于n,小于m

例:

1
2
3
4
复制代码// 循环5次,且步长为1的递增
for (i in 0 until 5){
print("i => $i \t")
}

输出结果为

1
复制代码i => 0  i => 1  i => 2  i => 3  i => 4
递减

关键字:downTo
范围:downTo[n,m] => 即小于等于n,大于等于m ,n > m
例:

1
2
3
4
复制代码// 循环5次,且步长为1的递减
for (i in 15 downTo 11){
print("i => $i \t")
}

输出结果为:

1
复制代码i => 15     i => 14     i => 13     i => 12     i => 11
符号(’ .. ‘) 表示递增的循环的另外一种操作

使用符号( ‘..’).
范围:..[n,m]=> 即大于等于n,小于等于m
和until的区别,一是简便性。二是范围的不同。
例:

1
2
3
4
5
6
7
8
9
复制代码print("使用 符号`..`的打印结果\n")
for (i in 20 .. 25){
print("i => $i \t")
}
println()
print("使用until的打印结果\n")
for (i in 20 until 25){
print("i => $i \t")
}

输出结果为:

使用 符号..的打印结果

1
复制代码i => 20     i => 21     i => 22     i => 23     i => 24     i => 25

使用until的打印结果

1
复制代码i => 20     i => 21     i => 22     i => 23     i => 24
设置步长

关键字:step

例:

1
2
3
复制代码for (i in 10 until 16 step 2){
print("i => $i \t")
}

输出结果为:

1
复制代码i => 10     i => 12     i => 14

迭代

for循环提供一个迭代器用来遍历任何东西。
for循环数组被编译为一个基于索引的循环,它不会创建一个迭代器对象

遍历字符串

此用法在数据类型章节中的字符串类型中用到过。还不甚清楚的可以查看 Kotlin——最详细的数据类型介绍。

例:

1
2
3
复制代码for (i in "abcdefg"){
print("i => $i \t")
}

输出结果为:

1
复制代码i => a  i => b  i => c  i => d  i => e  i => f  i => g
遍历数组

此用法在数据类型章节中的数组类型中用到过。还不甚清楚的可以查看 Kotlin——最详细的数据类型介绍。

例:

1
2
3
4
复制代码var arrayListOne = arrayOf(10,20,30,40,50)
for (i in arrayListOne){
print("i => $i \t")
}

输出结果为:

1
复制代码i => 10     i => 20     i => 30     i => 40     i => 50
使用数组的indices属性遍历

例:

1
2
3
4
复制代码var arrayListTwo = arrayOf(1,3,5,7,9)
for (i in arrayListTwo.indices){
println("arrayListTwo[$i] => " + arrayListTwo[i])
}

输出结果为:

1
2
3
4
5
复制代码arrayListTwo[0] => 1
arrayListTwo[1] => 3
arrayListTwo[2] => 5
arrayListTwo[3] => 7
arrayListTwo[4] => 9
使用数组的withIndex()方法遍历

例:

1
2
3
4
复制代码var arrayListTwo = arrayOf(1,3,5,7,9)
for ((index,value) in arrayListTwo.withIndex()){
println("index => $index \t value => $value")
}

输出结果为:

1
2
3
4
5
复制代码index => 0   value => 1
index => 1 value => 3
index => 2 value => 5
index => 3 value => 7
index => 4 value => 9
使用列表或数组的扩展函数遍历

数组或列表有一个成员或扩展函数iterator()实现了Iterator接口,且该接口提供了next()与hasNext()两个成员或扩展函数
其一般和while循环一起使用
可以查看Array.kt这个类。可以看见其中的iterator()函数,而这个函数实现了Iterator接口。

1
2
3
4
复制代码/**
* Creates an iterator for iterating over the elements of the array.
*/
public operator fun iterator(): Iterator<T>

查看Iterator.kt这个接口类,这个接口提供了hasNext()函数和next()函数。

1
2
3
4
5
6
7
8
9
10
复制代码public interface Iterator<out T> {
/**
* Returns the next element in the iteration.
*/
public operator fun next(): T
/**
* Returns `true` if the iteration has more elements.
*/
public operator fun hasNext(): Boolean
}

例:

1
2
3
4
5
复制代码var arrayListThree = arrayOf(2,'a',3,false,9)
var iterator: Iterator<Any> = arrayListThree.iterator()
while (iterator.hasNext()){
println(iterator.next())
}

输出结果为:

1
2
3
4
5
复制代码2
a
3
false
9

关于for循环的内容来自《Kotlin——最详细的控制语句详解》

9.kotlin stream 真心可以

流式处理给我们的集合操作带来了很大的方便,其实Java 8 一样支持流式处理,我只是想在这里推广一下 stream。

下面举例:

1
2
3
4
5
6
7
8
复制代码val names = arrayOf("Amy", "Alex", "Bob", "Cindy", "Jeff", "Jack", "Sunny", "Sara", "Steven")  

//筛选S开头的人名
val sName = names.filter { it.startsWith("S") }.toList()

//按首字母分组并排序
val nameGroup = names.groupBy { it[0] }
.toSortedMap( Comparator { key1, key2 -> key1.compareTo(key2) })

关于更多流式处理,请自行搜索Java stream

10.少写点方法重载

因为kotlin支持默认参数,所以在封装方法时会少很多的方法重载的。

如果没有默认参数的需要实现下面的日志打印,需要写多个方法:

1
2
3
4
5
6
复制代码fun log(tag: String, content: String) {
println("tag:$tag-->$content")
}
fun log( content: String) {
log("quanke","")
}

使用默认参数只需要一个方法:

1
2
3
复制代码fun log(tag: String="quanke", content: String) {
println("tag:$tag-->$content")
}

最后我还是想说:抱歉!不要用Java的语法思维来写Kotlin!

本文转载自: 掘金

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

大流量下订单号生成方法 流量不大的情况下,订单号生成 大流量

发表于 2018-03-08

流量不大的情况下,订单号生成


很久之前写过一篇利用DB生成业务主键的文章,介绍了利用DB来生成唯一的ID。当时便是用这种方式来生成订单号的。只不过拿到ID后,根据订单业务,简单加个前缀而已。

1
2
3
4
5
6
7
8
9
10
11
复制代码@Service
public class KeyGen{
@Autowired
private KeyGenRepository keyGenRepository;
public long genNo(){
KeyGen keyGen = new KeyGen();
keyGenRepository.genarateNo(keyGen );
//这个就是我们需要的no
long no = keyGen.getNo();
}
}
1
2
3
4
5
6
7
8
复制代码private String generateOrderNo() {
StringBuffer sbf=new StringBuffer();
//前缀
sbf.append("100");
long no = keyGen.genNo();
sbf.append(no);
return sbf.toString();
}

这种方式用了一段时间,没发现有订单号重复的情况。这种解决方案算是一个基础的思路,再复杂的订单生成规则,如果订单号要包含一个唯一的属性,利用数据库的自增特性是个不错的方案。


大流量下订单号的生成


如果每个小时的订单量非常大,比如说,一个小时有两百万个订单,只用单独一个key_gen表是支持不住的,毕竟写入的压力太大了,影响订单号的生成速度。这个时候可以考虑针对订单号的生成,搞单独的库,并分库,降低insert的压力,提高生成订单号的速度。


分库的规则


有一种做法是根据仓库来映射,比如说,一家电商公司的仓库总共有50个,那么可以进行如下的映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码warehouse1:数据库0
warehouse2:数据库0
warehouse3:数据库1
warehouse4:数据库1
warehouse5:数据库2
warehouse6:数据库2
warehouse7:数据库3
warehouse8:数据库3
warehouse9:数据库3
warehouse10:数据库4
。。。。。。
。。。。。。
warehouse50:数据库9

将50个仓库映射到【0-9】对应的数据库上,当下单的时候,如果订单对应的仓库的是warehouse1,则映射到数据库0,对应的仓库如果是warehouse10,则对应的数据库4。这样子,订单号的生成的压力便分配到10个数据库上了。

进行分库后,每个分库里都有一张key_gen表。


组装订单号


上面的分库分表,目的是为了生成一个唯一的ID,这个ID是订单号的一部分,生成ID借助了数据库,但是后面组装订单号则完全是业务逻辑操作,无需利用数据库了。

订单号的生成规则各个公司都有自己的要求,举个例子:

时间 + 6位随机数 + 数据库生成的唯一ID+仓库标识

时间的生成可以简单的使用如下代码生成:

1
2
3
复制代码SimpleDateFormat formatShort = new SimpleDateFormat("yyMMdd");
Date now = new Date();
String currentDate = formatShort.format(now);

六位的随机数可以借助JAVA的AtomicLong来实现,可以应付并发。

到此订单号完整的生成了。那有没有坑呢?因为进行了分库,每个库都有key_gen,生成的ID只是库内的唯一,多个库则是未必的。比如说,两个订单创建的请求,仓库分别是warehouse1和warehouse3,根据上面的配置规则,分别路由到了数据库0和数据库1这两个库,这个时候,就可能产生相同的ID。但是不要忘记,订单号的生成是包含仓库标识的,一个1,一个是3,是不同的,另外还有随机数,所以订单号重复的机会基本不太可能的。

本文转载自: 掘金

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

python多线程之从Thread类继承

发表于 2018-03-08

本文首发于知乎

上一篇文章实现多线程的方式是

  • 创建函数
  • 用Thread函数创建进程,将要运行的函数传入其中

本文我们讲另一种实现多线程的方式————从threading.Thread类继承出一个新类,主要实现__init__和run方法

一

首先我们来看一个最简单的例子,只实现了run方法

1
2
3
4
5
6
7
8
9
10
复制代码import time  
import threading
class MyThread(threading.Thread):
def run(self):
time.sleep(1)
a = 1 + 1
print(a)
for _ in range(5):
th = MyThread()
th.start()

总结一下

  • 定义一个类,继承threading.Thread类,里面只需要定义run方法
  • run方法相当于之前传入Thread的那个函数,注意只能用run这个名字,不用显式调用,线程start()时会自动调用run
  • 创建类的实例时不需要传入参数,得到的结果就可以调用start join等方法
  • 上一篇文章提过Thread对象可以调用start join run等方法,其实当时调用start就自动调用了run。这里只不过是在新类中重写了run方法,线程调用start时就会自动执行这个run

二

上面每次运行的run都是一样的,真正使用时很少会这样用,有时会需要传入一些区别性的参数,这就需要定义类的__init__了,我们来看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码import threading  
import requests
from bs4 import BeautifulSoup
class MyThread(threading.Thread):
def __init__(self, i):
threading.Thread.__init__(self)
self.i = i
def run(self):
url = 'https://movie.douban.com/top250?start={}&filter='.format(self.i*25)
r = requests.get(url)
soup = BeautifulSoup(r.content, 'html.parser')
lis = soup.find('ol', class_='grid_view').find_all('li')
for li in lis:
title = li.find('span', class_="title").text
print(title)
for i in range(10):
th = MyThread(i)
th.start()

上面代码实现10个线程抓取豆瓣top250网站10页的电影名,通过__init__将循环信息传到类之中。

三

上一篇文章不使用类来使用多线程时,讲了Thread函数的参数,Thread对象的方法和一些可以直接调用的变量,这里我们分别讲一下

  • Thread函数的参数。初始化线程时传入一些参数,这里也可以在__init__中定义
  • Thread对象的方法。这里可以用self直接调用这些方法
  • threading.activeCount()等直接调用的变量。在这里依然可以调用

所以用类的方法不会有任何限制,下面来看一个使用的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码import time  
import threading
class MyThread(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name
def run(self):
a = 1 + 1
print(threading.currentThread().name)
time.sleep(1)
print(self.name)
time.sleep(1)
print(self.is_alive())
t = time.time()
ths = [MyThread('thread {}'.format(i)) for i in range(3)]
for th in ths:
th.start()
print(threading.activeCount())
for th in ths:
th.join()
print(time.time() - t)

返回结果如下

1
2
3
4
5
6
7
8
9
10
11
复制代码thread 0  
thread 1
thread 2
4
thread 0
thread 2
thread 1
True
True
True
2.0039498805999756

四

使用类继承方式其实还有另一种形式。

之前是直接用run定义计算函数,如果已经有一个计算函数,也可以用传入的方式而不是改写成run

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码import threading  
import requests
from bs4 import BeautifulSoup
def gettitle(page):
url = 'https://movie.douban.com/top250?start={}&filter='.format(page*25)
r = requests.get(url)
soup = BeautifulSoup(r.content, 'html.parser')
lis = soup.find('ol', class_='grid_view').find_all('li')
for li in lis:
title = li.find('span', class_="title").text
print(title)
class MyThread(threading.Thread):
def __init__(self, target, **args):
threading.Thread.__init__(self)
self.target = target
self.args = args
def run(self):
self.target(**self.args)
for i in range(10):
th = MyThread(gettitle, page = i)
th.start()

欢迎关注我的知乎专栏

专栏主页:python编程

专栏目录:目录

版本说明:软件及包版本说明

本文转载自: 掘金

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

何时以及如何正确使用静态方法

发表于 2018-03-08

要知道何时以及如何正确使用静态方法,首先要搞清楚静态方法与实例方法的区别。静态(static)本身说明该行为是无状态的,无需实例化,调用时无需再格外分配内存来存放实例。所以,针对全局的单例场景、无状态的行为时,就可以考虑用使用静态方法。但是,静态方法有一个致命的问题,即它与具体类型是强耦合的。如果该行为可能存在变化,就要避免使用静态方法。

因此,有如下情形:

  • 对于确定不会变化的工具行为,使用静态方法;
  • 需要提供语法糖让代码更易于阅读,可以定义静态方法,因为它可以被static import。

例如,google的common库里面有一些工具类Files、Preconditions。这些工具类提供的方法通常是不会变的。如在Preconditions工具类中,非空检查的逻辑在将来并不会发生变化,该行为又没有状态,此时就可以用静态方法作为工具方法。

1
复制代码public static <T> T checkNotNull(T reference, @Nullable Object errorMessage) {    if(reference == null) {        throw new NullPointerException(String.valueOf(errorMessage));    } else {        return reference;    }}Preconditions.checkNotNull(name, "name is null");

对于第二种情形,例如单元测试时需要使用断言,为了提高测试代码的表现力,应力求测试更符合自然语言的阅读习惯。基于Java编写的AssertJ验证框架以拥有流畅的接口而著称。譬如说它提供的assertThat()方法,为了更好地体现DSL的特征,就被定义为静态方法,并通过static import隐藏类型信息,让方法调用变得更自然:

1
复制代码assertThat(fellowship).extracting("name")                      .contains("Boromir", "Gandalf", "Frodo", "Legolas")

如果不满足这两个条件,就尽量不要用静态方法,因为静态方法不利于扩展,不利于Mock,因而也不利于编写测试。至于针对一些无状态的服务方法,例如电商系统中针对促销策略的变化封装OnSalePolicy类,由于促销策略随时发生变化,因此也不能使用静态方法。最佳选择是定义为服务接口,然后通过依赖注入(Dependency Injection)实现松耦合,拥抱变化。

  • 本文作者: 张逸
  • 本文链接: zhangyi.xyz/when-use-st…
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!

本文转载自: 掘金

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

【翻译】漫谈C++游戏引擎开发 前言 采用迭代方法 先三思而

发表于 2018-02-28

前言


最近我用 C++ 写了一个游戏引擎,并用该引擎开发了一个名为 Hop Out 的小型手游。先来看看实际运行效果:

(译者注 这里本来有个小视频,但是没法直接展示,我想着转为 gif 格式总该可以了吧,结果还是不行。所以只好放到附件里了,感兴趣的朋友请下载观看,文件不到4MB)

Hop Out 是一款类似复古街机游戏,但拥有 3D 卡通外观的游戏。闯关方式为改变所有垫子的颜色,这一点和 Q*Bert 游戏很相似。

Hop Out 仍在开发当中,不过游戏引擎部分基本完工了,所以我想在这里分享关于游戏引擎开发的一些技巧。

在我看来,开发游戏引擎比较尴尬的一个情况就是你可能不知不觉地就造就出一个庞然大物,然后你一看到它就头皮发麻,所以我的主张是保持事物的可控性,具体将从以下三个方面进行阐述:

  • 采用迭代方法
  • 先三思而后合并
  • 认识到序列化是个很大的主题

采用迭代方法


我的第一条建议是先快速地让程序运行起来,然后迭代地进行开发。

如果条件允许的话,找个样例程序,然后以此为基础开始。以我为例,先下载 SDL 再打开 Xcode-iOS/Test/TestiPhoneOS.xcodeproj ,然后在 iPhone 上运行 testgles2 样例程序。立刻我就得到了一个很可爱的旋转立方体,如下图。

然后我下载一个别人做好的马里奥 3D 模型。随后编写了一个文件格式不太复杂的 OBJ 文件加载程序,接着修改样例程序,让马里奥取代立方体,如下图。还有,我集成了 SDL_image 来帮助加载纹理。

再然后,我实现了双摇杆控制来移动马里奥,如下图。

接下来我想着研究一下骨骼动画,所以我打开 Blender 制作了一个触手模型,并通过一段可以前后摆动的有两根骨头的骨架来操控它。

不过这里我放弃了使用 OBJ 文件格式,转而编写了一个将数据从 Blender 导出到自定义 JSON 文件的 Python 脚本,这些 JSON 文件存储了皮肤网格、骨骼、动画等数据。在 C++ JSON library 的帮助下我将这些文件加载到了游戏中。

上述过程成功后,我接着使用 Blender 制作更加精致的人物。下图展示了我制作出的第一个可操控的 3D 人物。

后来我又做了一大堆的工作,不过这里我想强调的重点是,我没有在动手编程之前先规划好引擎架构。事实上,每当要添加一个新特性时,我只着眼于用最简单的代码将其实现,然后观察这些代码,看看它们自然而然呈现出的是一种什么架构。这里所讲的引擎架构,指的是组成游戏引擎的模块集、模块之间的依赖关系,以及模块之间交互所使用的 API 。

这是一种迭代开发的方法,这种方法在编写游戏引擎时非常有用,其优点在于不管开发工作进行到哪个阶段,你始终都有一个可运行的程序。如果在后续提取代码模块时出现问题,你可以通过与上一次可正常运行的代码对比以快速地找出错误。显然,这里我假设你使用了某种源代码控制软件。

也许你认为这种开发方法会浪费大量的时间,因为中间过程会产生许多后续需要清理的垃圾代码。但是,大部分的清理工作无非就是将代码从一个 .cpp 文件移动到另一个 .cpp 文件、将函数声明提取到 .h 文件、或者一些其他简单的操作。决定代码的归属其实是一件相当困难的工作,但是显然,当代码呈现在你面前时,这个工作就会简单许多。

况且在我看来,先绞尽脑汁地想出一个你认为能满足未来所有需求的架构,然后再着手编程,会比迭代开发浪费更多的时间。这里推荐一下我最喜欢的关于介绍过度工程危害的两篇文章,一篇是 Tomasz Dąbrowski 的 The Vicious Circle of Generalization ,另一篇是 Joel Spolsky 的
Don’t Let Architecture Astronauts Scare You 。

但是请注意,我并没有说你永远都不应该先在纸面上解决问题,然后编程实现它。我也并没有说你不应该提前规划好你想要的功能。就我而言,我从一开始就想要游戏引擎能够在后台线程中加载所有 assets 文件,但是我一开始并没有去设计如何实现这个功能,而且一开始也确实没有实现这个功能,实际上我一开始只实现了加载部分 assets 文件的功能。

先三思而后合并


作为程序员,我们似乎会本能地避免代码重复、统一代码风格以让源代码看起来美观、优雅。然而,我的第二条建议是不要盲目地遵循这种本能。

给 DRY 原则放个假

为了给你一个示例,我的引擎包含了几个 smart pointer 模板类,类似于 std::shared_ptr 。通过作为一个 raw pointer 的包装器,它们个个都能防止内存泄漏。

  • Owned<> 用于被单个对象拥有的动态分配的对象。
  • Reference<> 使用引用计数来以便一个对象被多个对象拥有。
  • audio::AppOwned<> 被音频混频器外的代码使用。它允许游戏系统拥有音频混频器使用的对象,比如当前正在播放的声音。
  • audio::AudioHandle<> 使用一个引用计数系统内部的音频混频器。

看起来似乎这些类的功能有重复的地方,违背了 DRY(Don’t Repeat Yourself) 原则。事实确实如此,在开发早期,我曾想方设法地尽可能多地重用现有的 Reference<> 类。但是后来我发现音频对象的生命周期受一些特殊的规则控制:如果音频对象已经完成了播放,并且游戏也没有一个指向该音频对象的指针,那么该音频对象就可以立即排队等待删除了。如果游戏有一个指向该音频对象的指针,那么该音频对象就不该被删除。如果游戏有一个指向该音频对象的指针,但是该指针的拥有者在声音没有播放完成之前被破坏掉了,那么该声音就该被取消。我认为,与其增加Reference<>的复杂度,还不如引入单独的模板类,况且后者显然更实用一点。

95%的情况下,重用已有代码是没毛病的。然而,当你感觉到重用代码变了味、或者你正在把简单的东西变得复杂的时候,你就该仔细想想要不要坚持重用代码。

大胆地使用不同的调用约定

Java 有一点我很不喜欢,那就是每个函数都必须定义在类中。在我看来,这根本就是胡来,这样做也许使你的代码看起来更整齐一点,但其实它变相地鼓励了过度工程(over-engineering),而且也不能很好地支持我先前所提到地迭代开发方法。

在我的 C++ 引擎中,有些函数属于类,有些函数不属于类。例如,游戏中的每个敌人都是一个类,敌人的大多数行为都是在类中实现,但是球体滚动这个行为是通过调用函数 sphereCast() 实现的,该函数属于 physics 命名空间,但是函数 sphereCast() 并不属于任何类——它就是 physics 模块的一部分。我通过一个构建系统组织代码,该构建系统用于管理模块之间的依赖关系。将这个函数强行塞进一个类中对于改进代码组织来讲没多大意义。

再来谈谈多态(polymorphism)中的动态调度(dynamic dispatch)。我们经常需要在不知道对象确切类型的情况下调用函数获取对象。大多数 C++ 程序员的第一反应是使用虚函数定义抽象基类,然后在派生类中重载这些函数。这的确是一种行之有效的方法,但这只是实现该功能的众多方法中的一种罢了。还有一些可以不引入多余的代码,或者带有其他好处的动态调度技术:

  • C++11 引入了 std::function ,这是一种很方便的存储回调函数的方法。你还可以编写一个 std::function 个人版本,这样在调试器中单步执行时或许就没那么痛苦了。
  • 许多回调函数可以用一对指针来实现: 一个函数指针和一个 opaque 参数,只需要在回调函数内部进行显式转换即可。纯 C 库中有很多这种例子。
  • 有时侯, 底层类型实际上在编译时是已知的, 因此你可以绑定函数调用而无需额外的运行时开销。Turf ,是我在游戏引擎中使用的一个库, 就大量使用了这种技术。感兴趣的可以看看 turf::Mutex 。
  • 不过有时侯最直接的方法莫过于自己构建和维护一个原始函数指针表。我在音频混频器和序列化系统中使用了这种方法。正如下文将要提到的,Python 解释器也大量使用了此技术。
  • 甚至你可以将函数指针存储在哈希表中, 将函数名作为键。我使用此技术调度输入事件, 如多点触摸事件。这是一个记录游戏输入并使用回放系统重新播放策略的一部分。

动态调度是一个很大的课题,我只是随便举些例子罢了,实际上还有很多方法都可以实现。随着编写的可扩展底层代码(在开发游戏引擎中很常见)越来越多,你会探索出越来越多的方法。如果你不习惯这种编程方式,那么 Python 解释器或许对你来是是一个非常好的学习资源。它使用 C 编写,实现了一个强大的对象模型:每个 PyObject 都指向了一个 PyTypeObject ,而每个 PyTypeObject 都包含了一个用于动态调度的函数指针表。如果你感兴趣的话,可以从阅读文档
Defining New Types 开始。

认识到序列化是一个很大的主题


序列化(Serialization)指的是将运行时对象转化为字节序列,换句话讲,就是保存和加载数据。

对于许多游戏引擎来讲,游戏内容是以各种可编辑格式创建的,如 .png 、 .json 、 .blend 或者一些专有格式等,最终再将其转化为游戏引擎可以快速加载的平台特定的游戏格式。这个管道中的最后一个应用程序通常被称为 cooker 。cooker 也许会被集成到其他工具中,甚至分布在多台机器上。通常上,cooker 和许多工具是随游戏引擎本身一起开发和维护的。

在建立这样一个管道时,其中每个阶段的文件格式都由你设定。你也许会自己定义一些文件格式,这些文件格式可能会随着引擎功能的不断添加演变。随着它们的演变,有一天你或许会发现必须使某些程序与以前保存的文件格式保持兼容。但是,无论何种格式,你最终都得用 C++ 进行序列化。

C++ 实现序列化的方法数不胜数,一个比较容易想到的方法是在你想要序列化的 C++ 类中添加 load 函数和 save 函数。在文件头部中存储版本号,然后将版本号传递到每个 load 函数中,你就可以实现向后兼容性。这种办法可行,不过可能导致代码非常冗杂而难以维护。

1
2
3
4
5
6
7
8
9
10
复制代码void load(InStream& in, u32 fileVersion) {
        // Load expected member variables
        in >> m_position;
        in >> m_direction;
        // Load a newer variable only if the file version being loaded is 2 or greater
 
        if (fileVersion >= 2) {
            in >> m_velocity;
        }
    }

不过我们可以写出更灵活、更不容易出错的序列化代码,这里用到了反射(reflection),具体来讲是创建描述 C++ 类型布局的运行时数据。如果想要快速了解一下如何在序列化时使用反射,可以看看开源项目 Blender 。

当你从源代码构建 Blender 时,会发生许多事情。首先,一个名为 makesdna 的程序会被编译并运行。这个程序会解析 Blender 源树中的一组 C 头文件,然后输出一个包含了被称为 SDNA 的自定义格式的文件,该文件中存放了这些头文件内部定义的所有 C 类型的紧凑摘要,这些 SDNA 数据就是反射数据(reflection data)。然后 这些 SDNA 数据被链接到 Blender
,并和 Blender 所写的每个 .blend 文件一起保存。从此以后,每加载一个 .blend 文件,Blender 就会比较该 .blend 文件的 SDNA 数据与运行时链接到当前版本的 SDNA 数据,并使用通用序列化代码来处理差异。这种策略使得 Blender 的向前和向后兼容性非常强大。你可以在最新版中加载 1.0 版的文件,也可以在旧版本中加载新版本的 .blend 文件。

和 Blender 类似,许多游戏引擎和与之相关的工具都会生成并使用自己的反射数据。有很多方法做到这一点:你可以像 Blender 那样解析自己的 C/C++ 源代码来提取类型信息。你也可以创建一门独立的数据描述语言,并编写一个工具来生成此语言的 C++ 类型定义和反射数据。你还可以使用预处理器宏和 C++ 模板来生成运行时反射数据。一旦有了可用的反射数据,有无数种方法基于它编写一个通用序列化程序。

显然,我在此省略了许多细节。我只想说明确实有很多种方法来序列化数据,其中有一些方法是相当复杂的。程序员们通常并不会像讨论其他引擎系统那样讨论序列化,虽然事实上大部分其他的引擎系统都依赖序列化。例如,GDC 2017 上的96个编程会谈中,我统计了下,31个是关于图形学的,11个关于在线的,10个关于工具的,4个关于AI的,3个关于物理的,2个关于音频的,但是只有1个
直接涉及到了序列化。

总结


开发游戏引擎,哪怕规模很小,也是一项艰巨的任务。关于此我还有很多东西可说,但是考虑到博客长度,老实来讲,这就是我能想到的最实用的建议了:迭代开发、稍微控制一下统一代码的冲动、认识到序列化是一个很大的课题,你也许就能根据此确定出一个比较合适的策略了。根据我的经验,如果忽略了这些东西,它们很可能就会成为你的绊脚石。

(译者注 译文对原文有所删减,如有需要,请查看原文)
原文链接 how-to-write-your-own-cpp-game-engine

本文由 看雪翻译小组 hesir 编译

转载请注明来源

上传的附件:

  • hopoutclip.mp4 (3.66MB,140次下载)

本文转载自: 掘金

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

图书管理系统【JavaWeb 用户、购买、订单模块、添加权限

发表于 2018-02-27

用户模块

要登陆后才能购买,因此我们先写购买模块.

设计实体

1
2
3
4
5
6
7
8
9
复制代码
private String id;
private String username;
private String password;
private String email;
private String cellphone;
private String address;

//各种setter、getter

设计数据库表

1
2
3
4
5
6
7
8
9
10
11
复制代码
CREATE TABLE user (

id VARCHAR(40) PRIMARY KEY,
username VARCHAR(20) NOT NULL,
cellphone VARCHAR(20) NOT NULL,
address VARCHAR(40) NOT NULL,
email VARCHAR(30),
password VARCHAR(30) NOT NULL

);

##编写DAO ##

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
复制代码
/**
* 用户的登录注册模块
* 1:登陆
* 2:注册
* 3:根据id查找具体的用户
*/
public class UserDaoImpl {



public void register(User user) {

QueryRunner queryRunner = new QueryRunner(Utils2DB.getDataSource());

String sql = "INSERT INTO user (id,username,cellphone,address,email,password) VALUES(?,?,?,?,?,?)";
try {

queryRunner.update(sql, new Object[]{user.getId(),user.getUsername(), user.getCellphone(), user.getAddress(), user.getEmail(), user.getPassword()});
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

public User login(String username, String password) {

QueryRunner queryRunner = new QueryRunner(Utils2DB.getDataSource());

String sql = "SELECT * FROM user WHERE username = ? AND password=?";
try {

return (User) queryRunner.query(sql, new Object[]{username, password}, new BeanHandler(User.class));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

public User find(String id) {

QueryRunner queryRunner = new QueryRunner(Utils2DB.getDataSource());

String sql = "SELECT * FROM user WHERE id=?";
try {

return (User) queryRunner.query(sql, id, new BeanHandler(User.class));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

}

测试DAO

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
复制代码
public class UserDemo {
UserDaoImpl userDao = new UserDaoImpl();

@Test
public void add() {

User user = new User();
user.setId("1");
user.setUsername("zhong");
user.setPassword("123");
user.setCellphone("10085");
user.setAddress("广州萝岗");
user.setEmail("40368324234234@QQ.com");

userDao.register(user);
}

@Test
public void find() {

String id = "1";
User user = userDao.find(id);

System.out.println(user.getEmail());
}

@Test
public void login() {
String username = "zhong";
String password = "123";
User user = userDao.login(username, password);

System.out.println(user.getAddress());
}
}

抽取DAO

1
2
3
4
5
6
7
8
复制代码
public interface UserDao {
void register(User user);

User login(String username, String password);

User find(String id);
}

编写Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码
private UserDao userDao = DaoFactory.getInstance().createDao("zhongfucheng.dao.impl.UserDaoImpl", UserDao.class);

public void registerUser(User user) {
userDao.register(user);
}

public User loginUser(String username,String password) {
return userDao.login(username, password);
}

public User findUser(String id) {
return userDao.find(id);
}

前台样式

  • head.jsp
1
2
3
4
5
6
7
复制代码
<div id="User">
用户名:<input type="text" name="username">
密码:<input type="password" name="password">
<button name="login">登陆</button>
<button name="register">注册</button>
</div>
  • head.css
1
2
3
4
5
6
7
8
9
复制代码
#body {
position: relative;
}
#user {
position: absolute;
margin-top: 130px;
margin-left: 1364px;
}
  • 效果:

这里写图片描述


实现登陆注册功能

当点击登陆按钮的时候,把数据带过去给Servlet,让Servlet调用BusinessService方法,实现登陆。注册同理…..因此,我们需要用到JavaScript代码

  • head.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码
<c:if test="${user==null}" >
<div id="User">
用户名:<input type="text" id="username">
密码:<input type="password" id="password">
<button name="login" onclick="login()">登陆</button>
<button name="register" onclick="register()">注册</button>
</div>
</c:if>

<c:if test="${user!=null}" >
<div id="User">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;欢迎您:${user.username}&nbsp;&nbsp;&nbsp;&nbsp;<a href="${pageContext.request.contextPath}/UserServlet?method=Logout">注销</a>
</div>
</c:if>
  • javaScript代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码
<script type="text/javascript">

function login() {
//得到输入框的数据
var username = document.getElementById("username").value;
var password = document.getElementById("password").value;

//跳转到相对应的Servlet上
window.location.href = "${pageContext.request.contextPath}/UserServlet?method=login&username=" + username + "&password=" + password;
}

function register() {

//跳转到注册页面
window.location.href = "${pageContext.request.contextPath}/client/register.jsp";
}
</script>
  • UserServlet
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
复制代码

String method = request.getParameter("method");

BussinessServiceImpl service = new BussinessServiceImpl();
if (method.equals("login")) {

try {
//得到页面传递过来的数据
String username = request.getParameter("username");
String password = request.getParameter("password");
User user = service.loginUser(username, password);

request.getSession().setAttribute("user",user);
request.getRequestDispatcher("/client/head.jsp").forward(request, response);

} catch (Exception e) {
request.setAttribute("message", "登陆失败了!");
request.getRequestDispatcher("/message.jsp").forward(request, response);
}

} else if (method.equals("register")) {

try {
//得到JSP传递过来的数据,封装成Bean对象
User user = WebUtils.request2Bean(request, User.class);
user.setId(WebUtils.makeId());

service.registerUser(user);

request.setAttribute("message", "注册成功了!");

} catch (Exception e) {
e.printStackTrace();
request.setAttribute("message", "注册失败了!");
}
request.getRequestDispatcher("/message.jsp").forward(request, response);
} else if (method.equals("Logout")) {

//销毁session
request.getSession().invalidate();

//回到首页
request.getRequestDispatcher("/client/head.jsp").forward(request, response);

}

购买模块

在显示图书的时候,顺便添加购买的超链接

1
复制代码	<li><a href="#">购买</a></li>

设计购物车实体

如果不清楚为什么这样设计,可参考我之前的博文:blog.csdn.net/hon_3y/arti…

  • Cart实体
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
复制代码
public class Cart {

private Map<String, CartItem> map = new HashMap<>();
private double price;


//提供把商品添加到购物的功能
public void addBook2Cart(Book book) {

//得到对应的购物项
CartItem cartItem = map.get(book.getId());

//如果是null,说明购物车还没有该购物项
if (cartItem == null) {
cartItem = new CartItem();
cartItem.setQuantity(1);
cartItem.setBook(book);
cartItem.setPrice(book.getPrice());

//把购物项加到购物车中
map.put(book.getId(), cartItem);
} else {

//如果购物车有该购物项了,那么将购物项的数量+1
cartItem.setQuantity(cartItem.getQuantity() + 1);
}
}


//购物车的价钱是购物项价钱的总和
public double getPrice() {

double totalPrice = 0;
for (Map.Entry<String, CartItem> me : map.entrySet()) {
CartItem cartItem = me.getValue();
totalPrice += cartItem.getPrice();
}

return totalPrice;
}

public Map<String, CartItem> getMap() {
return map;
}

public void setMap(Map<String, CartItem> map) {
this.map = map;
}


public void setPrice(double price) {
this.price = price;
}
}

设计购物项实体

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
复制代码
public class CartItem {

private Book book;
private double price;
private int quantity;

public double getPrice() {
return this.book.getPrice() * this.quantity;
}

public void setPrice(double price) {
this.price = price;
}

public Book getBook() {
return book;
}

public void setBook(Book book) {
this.book = book;
}
public int getQuantity() {
return quantity;
}

public void setQuantity(int quantity) {
this.quantity = quantity;
}
}

处理用户想要买的书籍Servlet

1
2
复制代码	<li><a href="${pageContext.request
.contextPath}/BuyServlet?book_id=${book.id}">购买</a></li>
  • BuyServlet
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
复制代码
BussinessServiceImpl service = new BussinessServiceImpl();

//先检查该用户是否登陆了。
User user = (User) request.getSession().getAttribute("user");
if (user == null) {
request.setAttribute("message", "您还没登陆,请登陆了再来购买");
request.getRequestDispatcher("/message.jsp").forward(request, response);
return ;
}

//如果登陆了...

//得到该用户的购物车
Cart cart = (Cart) request.getSession().getAttribute("cart");
if (cart == null) {
cart = new Cart();
request.getSession().setAttribute("cart", cart);
}

//得到用户想买的书籍
String book_id = request.getParameter("book_id");
Book book = service.findBook(book_id);

//把书籍添加到购物车中
service.buyBook(cart, book);
request.setAttribute("message", "该商品已添加到购物车中");
request.getRequestDispatcher("/message.jsp").forward(request,response);

提供显示购物车商品的Servlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码
//先判断该用户是否登陆了
User user = (User) request.getSession().getAttribute("user");
if (user == null) {
request.setAttribute("message", "您还没有登陆呢!");
request.getRequestDispatcher("/message.jsp").forward(request, response);
return;
}

//如果登陆了.....
Cart cart = (Cart) request.getSession().getAttribute("cart");

//把该用户的购物车给JSP页面显示
request.setAttribute("cart", cart);
request.getRequestDispatcher("/client/listCart.jsp").forward(request, response);

显示购物车的JSP页面

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
复制代码
<c:if test="${empty(cart.map)}">

您还没有购买过任何商品哦!!!
</c:if>


<table border="1px">
<c:if test="${!empty(cart.map)}">
<h1>您购物车下有如下的商品:</h1><br>

<tr>
<td>书名:</td>
<td>作者:</td>
<td>数量:</td>
<td>价钱:</td>
</tr>
<c:forEach items="${cart.map}" var="cartItme">


<tr>
<td>${cartItme.value.book.name}</td>
<td>${cartItme.value.book.author}</td>
<td>${cartItme.value.quantity}</td>
<td>${cartItme.value.price}</td>
</tr>
</c:forEach>

</c:if>
</table>

效果:

这里写图片描述


订单模块

在前台用户界面中,当用户要把购物车付款时,应该提供生成订单的超链接….

这里写图片描述


设计订单实体

订单应该包含id,收货人信息,下单的时间,订单的总价,订单的状态【有无发货】..而不应该包含商品的信息的。商品的信息用一个专门的”订单项“来表示

一个订单对应多个订单项,这是一对多的关系!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码    private String id;

//下单的时间、日期、状态
private Date date;
private double price;
private boolean state;

//一个用户可以有多个订单,把用户记住
private String user_id;

//一个订单中有多个订单项
private Set<OrderItem> items = new HashSet<>();

//各种的setter和getter

设计订单项实体

1
2
3
4
5
6
7
8
9
10
11
复制代码
private String id;


//一本书对应多个订单项,订单项一定是由书组成,记住书
private String book_id;

private double price;
private int quantity;

//各种的setter和getter

设计数据库表

  • 订单表

mysql不能创建名为”order”的表,后边加个s就可以

1
2
3
4
5
6
7
8
9
10
复制代码

CREATE TABLE orders (
id VARCHAR(40) PRIMARY KEY,
date DATE NOT NULL,
user_id VARCHAR(40) NOT NULL,
state BOOLEAN,
price DOUBLE,
CONSTRAINT user_id_FK FOREIGN KEY (user_id) REFERENCES user (id)
);
  • 订单项表:
1
2
3
4
5
6
7
8
9
10
复制代码
CREATE TABLE orderItem (
id VARCHAR(40) PRIMARY KEY,
price DOUBLE,
quantity INT,
order_id VARCHAR(40) ,
book_id VARCHAR(40) ,
CONSTRAINT order_id_FK FOREIGN KEY (order_id) REFERENCES orders (id),
CONSTRAINT book_id_FK FOREIGN KEY (book_id) REFERENCES book (id)
);
  • 表之间的结构:

这里写图片描述


设计Dao

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
复制代码
public class OrderDaoImpl implements zhongfucheng.dao.OrderDao {

@Override
public void addOrder(Order order) {
QueryRunner queryRunner = new QueryRunner(Utils2DB.getDataSource());
String sql1 = "INSERT INTO orders(id,ordertime,user_id,state,price) VALUES(?,?,?,?,?)";
try {
//订单的基本信息
queryRunner.update(sql1, new Object[]{order.getId(), order.getOrdertime(), order.getUser_id(), order.isState(), order.getPrice()});

//订单项的信息
String sql2 = "INSERT INTO orderItem(id,price,quantity,order_id,book_id) VALUES(?,?,?,?,?)";

Set<OrderItem> items = order.getItems();

for (OrderItem item : items) {
queryRunner.update(sql2, new Object[]{item.getId(), item.getPrice(), item.getQuantity(), item.getOrder_id(), item.getBook_id()});
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

@Override
public Order findOrder(String id) {
QueryRunner queryRunner = new QueryRunner(Utils2DB.getDataSource());

Order order;
try {
//找出订单的基本信息
String sql = "SELECT * FROM orders WHERE id=?";
order = (Order) queryRunner.query(sql, new BeanHandler(Order.class), new Object[]{id});

//找出订单的所有订单项
String sql2 = "SELECT * FROM orderItem WHERE order_id=?";
List<OrderItem> list = (List<OrderItem>) queryRunner.query(sql2, new BeanListHandler(OrderItem.class), new Object[]{order.getId()});

System.out.println("这是数据库拿到的list集合:"+list.size());


//将所有订单项装到订单里边
order.getItems().addAll(list);
System.out.println("这是数据库拿到的"+order.getItems().size());


//找出该订单是属于哪一个用户的
String sql3 = "SELECT * FROM orders o,user u WHERE o.user_id=u.id AND o.id=? ";
User user = (User) queryRunner.query(sql3, new BeanHandler(User.class), new Object[]{order.getId()});

order.setUser_id(user.getId());
return order;


} catch (SQLException e) {
throw new RuntimeException(e);
}
}


//更新订单的状态
public void updateState(String id) {

QueryRunner queryRunner = new QueryRunner(Utils2DB.getDataSource());

String sql = "UPDATE orders SET state=? WHERE id=?";

try {
queryRunner.update(sql, new Object[]{true, id});
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

//查看已经发货或没发货的订单信息
public List<Order> getAllOrder(boolean state) {
QueryRunner queryRunner = new QueryRunner(Utils2DB.getDataSource());

String sql = "SELECT * FROM orders WHERE state=? ";
try {
return (List<Order>) queryRunner.query(sql, new BeanListHandler(Order.class), new Object[]{state});
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

//通过用户的id查找用户的订单,可能不止一个
public List<Order> findUserOrder(String user_id) {
QueryRunner queryRunner = new QueryRunner(Utils2DB.getDataSource());

String sql = "SELECT * FROM orders WHERE user_id=? ";

try {
return List<Order> queryRunner.query(sql, new BeanHandler(Order.class), new Object[]{user_id});

} catch (SQLException e) {
throw new RuntimeException(e);
}

}
}

二次更新

在编写dao的时候,尤其是Add方法。它是将所有数据都封装到Order对象上,然后取出数据,把数据插入到数据表中

  • 其实,我们的Order和OrderItem的操作可以分开。OrderItem也可以另外编写一个Dao,那么我们在插入完Order对象之后,得到Order对象返回的主键,再调用OrderItemDao的方法来插入OrderItem的数据,这样我觉得会让代码清晰一些。
  • 在OrderItemDao中接收的是一个List,因为我们一个订单会对应多个订单项。

抽取成DAO接口

1
2
3
4
5
6
7
8
9
10
11
12
复制代码
public interface OrderDao {
void addOrder(Order order);

Order findOrder(String id);

List<Order> getAllOrder(boolean state);

void updateState(String user_id);

List<Order> findUserOrder(String user_id);
}

BussinessService

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
复制代码    private OrderDao orderDao = DaoFactory.getInstance().createDao("zhongfucheng.dao.impl.OrderDaoImpl", OrderDao.class);
public void createOrder(Cart cart, User user) {

//订单的基本信息
String order_id = WebUtils.makeId();
Order order = new Order();
order.setId(order_id);
order.setPrice(cart.getPrice());
order.setOrdertime(new Date());
order.setState(false);
order.setUser_id(user.getId());


//订单项的基本信息
//得到每个购物项,购物项就作为订单项
for (Map.Entry<String, CartItem> me : cart.getMap().entrySet()) {

OrderItem orderItem = new OrderItem();
CartItem cartItem = me.getValue();

orderItem.setId(WebUtils.makeId());
orderItem.setPrice(cartItem.getPrice());
orderItem.setBook_id(cartItem.getBook().getId());
orderItem.setQuantity(cartItem.getQuantity());
orderItem.setOrder_id(order_id);
order.getItems().add(orderItem);
}

orderDao.addOrder(order);

}

public Order findOrder(String user_id) {

return orderDao.findOrder(user_id);
}

public List<Order> getAllOrder(boolean state) {
return orderDao.getAllOrder(state);
}

public void sendOutOrder(String id) {

orderDao.updateState(id);
}

public List<Order> findUserOrder(String user_id) {
return orderDao.findUserOrder(user_id);
}

生成订单的Servlet

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

BussinessServiceImpl service = new BussinessServiceImpl();

//检查该用户的购物车是否有商品
Cart cart = (Cart) request.getSession().getAttribute("cart");
if (cart == null) {
request.setAttribute("message", "您购物车没有商品,无法生成订单");
request.getRequestDispatcher("/message.jsp").forward(request, response);
return;
}

//如果有商品,得到当前用户
User user = (User) request.getSession().getAttribute("user");
service.createOrder(cart, user);
request.setAttribute("message", "订单已经生成了,准备好钱来收货把");
request.getRequestDispatcher("/message.jsp").forward(request, response);
return;

用户查询自己的订单Servlet

1
2
复制代码
<a href="${pageContext.request.contextPath}/LookOrder" target="body">查看订单</a>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码
BussinessServiceImpl service = new BussinessServiceImpl();

//检查该用户是否登陆了
User user = (User) request.getSession().getAttribute("user");
if (user == null) {
request.setAttribute("message", "您还没登陆,等您登陆了再来看把");
request.getRequestDispatcher("/message.jsp").forward(request, response);
return;
}

//用户登陆了!
Order order = service.findUserOrder(user.getId());

//交给相对应的JSP 显示
request.setAttribute("order", order);
request.setAttribute("user",user);
request.getRequestDispatcher("/client/listOrder.jsp").forward(request, response);
return ;

显示订单数据的JSP

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
复制代码
<c:if test="${order==null}">
您还没有下过任何订单!!

</c:if>

<c:if test="${order!=null}">

<table border="1px">

<tr>
<td>下单人:</td>
<td>订单时间</td>
<td>订单状态</td>
<td>订单价钱</td>
</tr>
<tr>
<td>${user.username}</td>
<td>${order.ordertime}</td>
<td>${order.state==false?"未发货":"已发货"}</td>
<td>${order.price}</td>
</tr>

</table>

</c:if>

效果:

这里写图片描述


后台查询订单的状况Servlet

1
2
3
复制代码
<a href="${pageContext.request.contextPath}/OrderServlet?state=false" target="body">待处理订单</a><br>
<a href="${pageContext.request.contextPath}/OrderServlet?state=true" target="body">已发货订单</a><br>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码
BussinessServiceImpl service = new BussinessServiceImpl();
String state = request.getParameter("state");

if (state.equals("true")) {
List<Order> list = service.getAllOrder(true);
request.setAttribute("list",list);

} else if (state.equals("false")) {
List<Order> list = service.getAllOrder(false);
request.setAttribute("list", list);
}


request.getRequestDispatcher("/background/listOrder.jsp").forward(request, response);

显示订单状况的JSP

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
复制代码
<c:if test="${empty(list)}">

还没有任何订单哦!

</c:if>

<c:if test="${!empty(list)}">

<table border="1px">

<tr>
<td>下单人:</td>
<td>订单时间</td>
<td>订单状态</td>
<td>订单价钱</td>
<td>操作</td>
</tr>

<c:forEach items="${list}" var="order">
<tr>
<td>${order.user_id}</td>
<td>${order.ordertime}</td>
<td>${order.state==false?"未发货":"已发货"}</td>
<td>${order.price}</td>
<td>
<a href="${pageContext.request.contextPath}/orderItemServlet?order_id=${order.id}">查看详细信息</a>
<a href="#">删除</a>

</td>
</tr>
</c:forEach>


</table>


</c:if>

查看具体订单的详细信息Servlet

1
2
3
4
5
6
7
8
9
10
11
复制代码
BussinessServiceImpl service = new BussinessServiceImpl();

//得到用户想要查看详细信息的表单
String order_id = request.getParameter("order_id");

Order order = service.findOrder(order_id);

//将该order对象给相对应的JSP显示
request.setAttribute("order", order);
request.getRequestDispatcher("/background/listDetail.jsp").forward(request, response);

查看具体订单的详细信息JSP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码
<table border="1px">

<tr>
<td>书籍的编号</td>
<td>价钱</td>
<td>数量</td>
<td>操作</td>
</tr>
<c:forEach items="${order.items}" var="item">
<tr>
<td>${item.book_id}</td>
<td>${item.price}</td>
<td>${item.quantity}</td>
<td><a href="${pageContext.request.contextPath}/SendOutServlet?id=${order.id}">发货</a></td>
</tr>
</c:forEach>
</table>

处理发货的Servlet##

1
2
3
4
5
6
7
8
9
复制代码

BussinessServiceImpl service = new BussinessServiceImpl();
String id = request.getParameter("id");

service.sendOutOrder(id);
request.setAttribute("message", "已发货!");
request.getRequestDispatcher("/message.jsp").forward(request, response);
return;

效果:

这里写图片描述


添加权限控制

目前为止,我们已经学习了动态代理技术和注解技术了。于是我们想要为之前的bookStore项目添加权限控制…..

只有用户有权限的时候,后台管理才可以进行相对应的操作…..


实现思路

这里写图片描述

之前我们做权限管理系统的时候,是根据用户请求的URI来判断该链接是否需要权限的。这次我们使用动态代理的技术和注解来判断:用户调用该方法时,检查该方法是否需要权限…

根据MVC模式,我们在web层都是调用service层来实现功能的。那么我们具体的思路是这样的:

  • web层调用service层的时候,得到的并不是ServiceDao对象,而是我们的代理对象
  • 在service层中的方法添加注解,如果方法上有注解,那么说明调用该方法需要权限…
  • 当web层调用代理对象方法的时候,代理对象会判断该方法是否需要权限,再给出相对应的提示….

设计实体、数据库表

上次我们做的权限管理系统是引入了角色这个概念的,这次主要为了练习动态代理和注解技术,就以简单为主,不引入角色这个实体。直接是用户和权限之间的关系了。

Privilege实体

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

private String id ;
private String name;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

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

数据库表

  • privilege表
1
2
3
4
5
6
7
8
复制代码

CREATE TABLE privilege (

id VARCHAR(40) PRIMARY KEY,
name VARCHAR(40)

);

privilege和user是多对多的关系,于是使用第三方表来维护他们的关系

  • user_privilege表
1
2
3
4
5
6
7
8
9
10
11
复制代码

CREATE TABLE user_privilege (
privilege_id VARCHAR(40),
user_id VARCHAR(40),

PRIMARY KEY (privilege_id, user_id),
CONSTRAINT privilege_id_FK FOREIGN KEY (privilege_id) REFERENCES privilege(id),
CONSTRAINT user_id_FK1 FOREIGN KEY (user_id) REFERENCES user(id)

);

添加测试数据

为了方便,直接添加数据了。就不写详细的DAO了。

  • 在数据库中添加了两个权限

这里写图片描述

  • 为id为1的user添加了两个权限

这里写图片描述


编写DAO

后面在动态代理中,我们需要检查该用户是否有权限…那么就必须查找出该用户拥有的哪些权限。再看看用户有没有相对应的权限

1
2
3
4
5
6
7
8
9
10
11
复制代码	//查找用户的所有权限
public List<Privilege> findUserPrivilege(String user_id) {
QueryRunner queryRunner = new QueryRunner(Utils2DB.getDataSource());

String sql = "SELECT p.* FROM privilege p, user_privilege up WHERE p.id = up.privilege_id AND up.user_id = ?";
try {
return (List<Privilege>) queryRunner.query(sql, new Object[]{user_id}, new BeanListHandler(Privilege.class));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

抽取到接口上

1
2
复制代码
List<Privilege> findUserPrivilege(String user_id);

注解模块

  • 编写注解
1
2
3
4
复制代码@Retention(RetentionPolicy.RUNTIME)
public @interface permission {
String value();
}
  • 在Service层方法中需要权限的地方添加注解CategoryServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码
@permission("添加分类")
/*添加分类*/
public void addCategory(Category category) {
categoryDao.addCategory(category);
}


/*查找分类*/
public void findCategory(String id) {
categoryDao.findCategory(id);
}

@permission("查找分类")
/*查看分类*/
public List<Category> getAllCategory() {
return categoryDao.getAllCategory();
}

抽取Service

把Service的方法抽取成ServiceDao。在Servlet中,也是通过ServiceFactory来得到Service的对象【和DaoFactory是类似的】

CategoryService

1
2
3
4
5
6
7
8
9
复制代码
@permission("添加分类")
/*添加分类*/ void addCategory(Category category);

/*查找分类*/
void findCategory(String id);

@permission("查找分类")
/*查看分类*/ List<Category> getAllCategory();

ServiceFactory

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
复制代码
public class ServiceDaoFactory {

private static final ServiceDaoFactory factory = new ServiceDaoFactory();

private ServiceDaoFactory() {
}

public static ServiceDaoFactory getInstance() {
return factory;
}


//需要判断该用户是否有权限
public <T> T createDao(String className, Class<T> clazz, final User user) {

System.out.println("添加分类进来了!");

try {
//得到该类的类型
final T t = (T) Class.forName(className).newInstance();
//返回一个动态代理对象出去
return (T) Proxy.newProxyInstance(ServiceDaoFactory.class.getClassLoader(), t.getClass().getInterfaces(), new InvocationHandler() {

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, PrivilegeException {
//判断用户调用的是什么方法
String methodName = method.getName();
System.out.println(methodName);

//得到用户调用的真实方法,注意参数!!!
Method method1 = t.getClass().getMethod(methodName,method.getParameterTypes());

//查看方法上有没有注解
permission permis = method1.getAnnotation(permission.class);

//如果注解为空,那么表示该方法并不需要权限,直接调用方法即可
if (permis == null) {
return method.invoke(t, args);
}

//如果注解不为空,得到注解上的权限
String privilege = permis.value();

//设置权限【后面通过它来判断用户的权限有没有自己】
Privilege p = new Privilege();
p.setName(privilege);

//到这里的时候,已经是需要权限了,那么判断用户是否登陆了
if (user == null) {

//这里抛出的异常是代理对象抛出的,sun公司会自动转换成运行期异常抛出,于是在Servlet上我们根据getCause()来判断是不是该异常,从而做出相对应的提示。
throw new PrivilegeException("对不起请先登陆");
}

//执行到这里用户已经登陆了,判断用户有没有权限
Method m = t.getClass().getMethod("findUserPrivilege", String.class);
List<Privilege> list = (List<Privilege>) m.invoke(t, user.getId());

//看下权限集合中有没有包含方法需要的权限。使用contains方法,在Privilege对象中需要重写hashCode和equals()
if (!list.contains(p)) {
//这里抛出的异常是代理对象抛出的,sun公司会自动转换成运行期异常抛出,于是在Servlet上我们根据getCause()来判断是不是该异常,从而做出相对应的提示。
throw new PrivilegeException("您没有权限,请联系管理员!");
}

//执行到这里的时候,已经有权限了,所以可以放行了
return method.invoke(t, args);
}
});

} catch (Exception e) {
new RuntimeException(e);
}
return null;
}
}

PrivilegeExcetption

当用户没有登陆或者没有权限的时候,我们应该给用户一些友好的提示….于是我们自定义了PrivilegeException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码
public class PrivilegeException extends Exception {

public PrivilegeException() {
super();
}

public PrivilegeException(String message) {
super(message);
}

public PrivilegeException(String message, Throwable cause) {
super(message, cause);
}

public PrivilegeException(Throwable cause) {
super(cause);
}
}

我们继承的是Exception,通过方法名抛出去。但是我们是通过代理对象调用方法的,于是sun公司的策略就是把它们转换成运行期异常抛出去。

因此,我们就在Servlet上得到异常,再给出友好的提示。。


效果:

  • 没有登陆的时候:

这里写图片描述

  • 登陆了,但是没有相对应的权限的时候

这里写图片描述

  • 登陆了,并且有权限

这里写图片描述

要点总结

该权限控制是十分优雅的,只要我在Service层中添加一个注解…那么当web层调用该方法的时候就需要判断用户有没有该权限….

  1. 外界调用Service层的方法是代理调用invoke()方法,我们在invoke()方法可以对其进行增强!
  2. invoke()方法内部就是在查询调用该方法上有没有注解,如果没有注解,就可以直接调用。如果有注解,那么就得到注解的信息,判断该用户有没有权限来访问这个方法
  3. 在反射具体方法的时候,必须记得要给出相对应的参数!
  4. 在invoke()方法抛出的编译时期异常,java会自动转换成运行期异常进行抛出…
  5. 使用contains()方法时,就要重写该对象的hashCode()和equals()

本文转载自: 掘金

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

Python 系列教程(入门系列已写完) FQ Python

发表于 2018-02-27

IT 行业相对于一般传统行业,发展更新速度更快,一旦停止了学习,很快就会被行业所淘汰,但是,我们要清楚:淘汰的永远只是那些初级水平的从业者,过硬技术的从业者永远都是稀缺的。因此对于学习,我们还是要踏踏实实的。

自学 Python ,也是一样,不要一开始因为头脑发热就不停地收藏各种资料网站,购买各种书籍,下载了大量的教学视频,过了几天,学习的热情开始褪去,再过几个星期,终于完成了学习课程 —— 《从入门到放弃》。所以,学习 Python 需要一步一个脚印,踏踏实实地学。

FQ

在讲 Python 如何入门之前,个人建议最好每个人都有自己的 FQ 工具,多使用 Google 搜索,多去看一下墙外的世界,多看 Github 上的开源项目。

至于如何 FQ ,这里提供一下我用过的工具:FQ工具集

Python 学习资源集

相信很多人学习某门编程语言的时候,都会找各种学习资料。说句实话,资料太多,反而没用,根据自己的学习习惯,个人能力选择一门资源坚持学就好了。

因为每个人的需求不同,这里整理了一批 Python 比较好的学习资料:

  • Python 博客网站资源
  • Python 视频教程资源

Python 入门

对于入门,主要是掌握基本的语法和熟悉编程规范,因此大部分的教程基本一致的,所以还是建议选好适合自己的一个教程,坚持学下去。

在 Python 入门中,本人自己编写了一系列的 《 草根学 Python 》 博客, 是基于 Python 3.6 写的 Python 入门系列教程,为了更好的阅读,把它整理在 GitBook 上;如果 GitBook 访问慢,也可以到个人博客查阅:草根学 Python,希望对各位入门 Python 有所帮助。

主要目录如下:

  • Python代码规范
    • 简明概述
    • 注释
    • 命名规范
  • 第一个Python程序
    • Python 简介
    • Python 的安装
    • 第一个 Python 程序
    • 集成开发环境(IDE): PyCharm
  • 基本数据类型和变量
    • Python 语法的简要说明
    • Python 的基本数据类型
    • 基本数据类型转换
    • Python 中的变量
  • List 和 Tuple
    • List(列表)
    • tuple(元组)
  • Dict 和 Set
    • 字典(Dictionary)
    • set
  • 条件语句和循环语句
    • 条件语句
    • 循环语句
    • 条件语句和循环语句综合实例
  • 函数
    • Python 自定义函数的基本步骤
    • 函数传值问题
    • 函数返回值
    • 函数的参数
    • 匿名函数
  • 迭代器和生成器
    • 迭代
    • Python 迭代器
    • lsit 生成式(列表生成式)
    • 生成器
    • 迭代器和生成器综合例子
  • 模块与包
    • Python 模块简介
    • 模块的使用
    • 主模块和非主模块
    • 包
    • 作用域
  • 面向对象
    • 面向对象的概念
    • 类
    • 类的属性
    • 类的方法
    • 类的继承
    • 类的多态
  • Python 的 Magic Method
    • Python 的 Magic Method
    • 构造(__new__)和初始化(__init__)
    • 属性的访问控制
    • 对象的描述器
    • 自定义容器(Container)
    • 运算符相关的魔术方法
  • 枚举类
    • 枚举类的使用
    • Enum 的源码
    • 自定义类型的枚举
    • 枚举的比较
  • 元类
    • Python 中类也是对象
    • 使用 type() 动态创建类
    • 什么是元类
    • 自定义元类
    • 使用元类
  • 线程与进程
    • 线程与进程
    • 多线程编程
    • 进程
  • 一步一步了解正则表达式
    • 初识 Python 正则表达式
    • 字符集
    • 数量词
    • 边界匹配符和组
    • re.sub
    • re.match 和 re.search
  • 闭包
  • 装饰器

知识点补漏

  • Python 关键字 yield

Python 进阶

持续更新….

关注的我的 github ,实时更新

本文转载自: 掘金

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

1…897898899…956

开发者博客

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