关于编程语言模块的一些介绍
| 命令 | 描述 |
|---|---|
关于编程语言模块的一些介绍
| 命令 | 描述 |
|---|---|

流水灯模块使用P1 进行IO操作。并使用共地。默认高电平 所以需要点亮LED等, 就是将P1口 地线接通即可。
P1引脚一共8位。此处变量LED设置P1.0(即P1引脚第0位)
#include <reg52.h>
sbit LED = P1^0; // 定义P1.0为LED引脚
void main() {
LED = 0; // 输出低电平,点亮LED(共地接法)
while (1); // 保持程序运行
}
#include <reg52.h>
sbit LED1 = P1^0;
sbit LED3 = P1^2;
sbit LED5 = P1^4;
void main() {
LED1 = 0;
LED3 = 0;
LED5 = 0;
while (1);
}
#include <reg52.h>
void main() {
// 0xEA 在二进制表示为 11101010
P1 = 0xEA;
while (1);
}
#include <reg52.h>
unsigned char a;
void delay();
void main() {
while(1) {
delay();
for(a = 0; a < 8; a++) {
P1 = P1 << 1;
delay();
}
P1 = 0xFF;
}
}
void delay() {
int i = 65535;
while(i--);
}
这里要用到一个 intrins 的 corl 循环左移 corr 循环右移 nop 空指令
#include <reg52.h>
#include <intrins.h>
unsigned char a;
unsigned char temp;
void delay();
void main() {
temp = 0xFE;
P1 = temp;
while(1) {
delay();
temp = _crol_(temp, 1);
P1 = temp;
}
}
void delay() {
int i = 65535;
while(i--);
}
#include <reg52.h>
#include <intrins.h>
unsigned char a;
unsigned char temp;
void delay(unsigned int z);
void main() {
temp = 0x7F;
P1 = temp;
while(1) {
delay(1000);
temp = _cror_(temp, 1);
P1 = temp;
}
}
// 改进了延时函数
void delay(unsigned int z) {
unsigned int x,y;
for(x = z; x > 0; x--)
for(y=0; y<125; y++);
}
#include <reg52.h>
#include <intrins.h>
unsigned char a;
unsigned char temp;
void delay(unsigned int z);
void main() {
temp = 0xFD;
P1 = temp;
while(1) {
delay(1000);
temp = _crol_(temp, 2);
P1 = temp;
}
}
void delay(unsigned int z) {
unsigned int x,y;
for(x = z; x > 0; x--)
for(y=0; y<125; y++);
}
引用 轮流点亮每一个LED,每次只点亮一个 从右向左
根据开发板原理图 数码管由两个锁存器共同控制

型号为74HC573, 接下来看锁存器说明

带上划线的OE, 表示低电平有效
所以看表当OE为高电平时,不论LE D 如何 输出都是高阻态(用Z表示)
当OE为底电平时,LE为低电平时, 此时就锁住了输出值(用Q0 表示) 当OE为底电平时,LE为高电平时, 此时输出随D口变化而变化也就是连通状态
回到我们的锁存器电路图,OE均接地,所以已经是低电平有效。所以我们要控制的就是LE 让她为高电平就变化锁存器里面的值。之后改成低电平来锁住值。
我们的单片机数码管是8位共阴极数码管
U8锁存器是作为位选,U9 锁存器是段选
段选是表示 数码管要显示什么数字 位选(位置选择)是表示 一个8段数码管(每一个8. 作为一段)那些需要点亮
例如我们要显示 从右侧开始 1357段数码管显示数字5
#include <reg52.h>
#include <intrins.h>
void delay(unsigned int z);
sbit DULA = P2^6; // 段选LE控制
sbit WELA = P2^7; // 位选LE控制
unsigned char code leddata[]={
0x3F, //"0"
0x06, //"1"
0x5B, //"2"
0x4F, //"3"
0x66, //"4"
0x6D, //"5"
0x7D, //"6"
0x07, //"7"
0x7F, //"8"
0x6F //"9"
};
void main() {
DULA = 1;
P0 = leddata[5];
DULA = 0;
delay(10);
WELA = 1;
P0 = 0x55;
WELA = 0;
while(1);
}
void delay(unsigned int z) {
unsigned int x,y;
for(x = z; x > 0; x--)
for(y=0; y<125; y++);
}
另一个例子 从0-9 循环显示 每次间隔1s
#include <reg52.h>
#include <intrins.h>
void delay(unsigned int z);
sbit DULA = P2^6;
sbit WELA = P2^7;
unsigned char code leddata[]={
0x3F, //"0"
0x06, //"1"
0x5B, //"2"
0x4F, //"3"
0x66, //"4"
0x6D, //"5"
0x7D, //"6"
0x07, //"7"
0x7F, //"8"
0x6F //"9"
};
void main() {
unsigned int i;
delay(10);
WELA = 1;
P0 = 0x00;
WELA = 0;
while(1){
for(i = 0; i <= 9; i++) {
DULA = 1;
P0 = leddata[i];
DULA = 0;
delay(1000);
}
}
}
void delay(unsigned int z) {
unsigned int x,y;
for(x = z; x > 0; x--)
for(y=0; y<125; y++);
}
所谓的动态显示就是利用人眼的视觉 延时误差来让数码管看起来保持不变 例如我们要让左侧 第一个显示1 第二个显示2 在数码管实例图中 WE1 口对应Q0, Q0对应D0,所以将位选的D0 置为0(二进制(大端表示)表示1111 1110),段选设置为1 WE2 对应Q1 Q1对应D1,所以将位选的D1 置为1(二进制(大端表示)表示 1111 1101),段选设置为2 如此快速的往复,即可实现数码管显示1和2。代码如下
#include <reg52.h>
#include <intrins.h>
void delay(unsigned int z);
sbit DULA = P2^6;
sbit WELA = P2^7;
unsigned char code leddata[]={
0x3F, //"0"
0x06, //"1"
0x5B, //"2"
0x4F, //"3"
0x66, //"4"
0x6D, //"5"
0x7D, //"6"
0x07, //"7"
0x7F, //"8"
0x6F //"9"
};
void main() {
while(1){
WELA = 1;
P0 = 0xFE;
WELA = 0;
DULA = 1;
P0 = leddata[1];
DULA = 0;
delay(5);
WELA = 1;
P0 = 0xFD;
WELA = 0;
DULA = 1;
P0 = leddata[2];
DULA = 0;
delay(5);
}
}
void delay(unsigned int z) {
unsigned int x,y;
for(x = z; x > 0; x--)
for(y=0; y<125; y++);
}
下一个例子, 在八个数码管从左到右 一次显示 1 2 3 4 5 6 7 8
#include <reg52.h>
#include <intrins.h>
void delay(unsigned int z);
sbit DULA = P2^6;
sbit WELA = P2^7;
unsigned char code leddata[]={
0x3F, //"0"
0x06, //"1"
0x5B, //"2"
0x4F, //"3"
0x66, //"4"
0x6D, //"5"
0x7D, //"6"
0x07, //"7"
0x7F, //"8"
0x6F, //"9"
0x77, //"A"
0x7C, //"B"
0x39, //"C"
0x5E, //"D"
0x79, //"E"
0x71, //"F"
0x76, //"H"
0x38, //"L"
0x37, //"n"
0x3E, //"u"
0x73, //"P"
0x5C, //"o"
0x40, //"-"
0x00, //熄灭
0x00 //自定义
};
unsigned char code dudata[] = {
0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F
};
void main() {
unsigned char i = 0;
while(1){
WELA = 1;
P0 = dudata[i];
WELA = 0;
DULA = 1;
P0 = leddata[i+1];
DULA = 0;
i++;
delay(1);
if (i == 8) {
i = 0;
}
}
}
void delay(unsigned int z) {
unsigned int x,y;
for(x = z; x > 0; x--)
for(y=0; y<125; y++);
}
SequentialList 顺序表 (SeqList)
LinearList 线性表
LinkedList 链表
SingleLinkedList 单链表
CircularLinkedList 循环链表
InitList (&L) 构造一个空的线性表L。
DestroyList(&L) 如果线性表L已存在。 销毁线性表L。
ClearList (&L) 如果线性表L已存在。 将L重置为空表。
ListEmpty(L) 如果线性表L已存在。 若L为空表, 则返回true, 否则返回false。
ListLength(L) 线性表L已存在。 返回L中数据元素个数(长度)。
GetElem(L,i,&e) 线性表L巳存在,且1 <= i <= ListLength(L)。 用e返回L中第i个数据元素的值。
LocateElem(L,e) 线性表L已存在。 返回L中第1个 值与e相同的元素在 L中的位置 。若这样的数据元素不存在 , 则返回值为0。
PriorElem(L,cur_e,&pre_e) 线性表L已存在。 cur_e是L的元素且不是第一个,则用pre_e返回其前驱,否则操作失败,pre_e无定义。
NextElem(L,cur_e,&next_e) 线性表L已存在。若cur_e是L的数据元素,且不是最后一个,则用next_e返回其后继,否则操作失败,next_e无定义。
Listinsert(&L,i,e) 线性表L已存在,且1<=i<=ListLength (L) +l。在 L中第i个位置之前插入新的数据元素 e, L的长度加1。
ListDelete(&L,i) 线性表L已存在且非空 ,且且1<=i<=ListLength (L)。删除L的第i个数据元素,L的长度减1。
TraverseList(L) 线性表L已存在。遍历线性表
线性表的顺序表示是使用一组地址连续的存储单元一次存储线性表的数据元素。通常这种存储结构成为顺序表。其特点是,逻辑上相邻的数据元素, 其物理次序也是相邻的。
例如每个元素占用空间是5,假设从地址0开始,则第1个元素占用0-4号地址,第2个元素将占用5-9号地址
顺序表存储结构如下,一元多项式举例略,
#define MAXSIZE 100 //图书表最大长度
typedef struct
{
char no[20];//图书ISBN
char name[50];//图书名称
float price;//图书价格
} Book;//图书的定义
typedef struct
{
Book *elem;//存储空间的基地址
int length;//图书表中当前图书个数
} SqList;//图书表的顺序存储结构类型
int InitList(SqList *L)
{
L->elem = (Book*)malloc(MAXSIZE*(sizeof(float)+20+70));
printf("%p\n", L->elem);
if (L->elem==NULL)
{
return -1;
}
L->length = 0;
return 0;
}
关于编程语言模块的一些介绍
func goID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
// 先声明map
var m1 map[string]string
// 再使用make函数创建一个非nil的map,nil map不能赋值
m1 = make(map[string]string)
// 最后给已声明的map赋值
m1["a"] = "aa"
m1["b"] = "bb"
// 直接创建
m2 := make(map[string]string)
// 然后赋值
m2["a"] = "aa"
m2["b"] = "bb"
// 初始化 + 赋值一体化
m3 := map[string]string{
"a": "aa",
"b": "bb",
}
if v, ok := m1["a"]; ok {
fmt.Println(v)
} else {
fmt.Println("Key Not Found")
}
for k, v := range m1 {
fmt.Println(k, v)
}
scene := make(map[string]int)
// 准备map数据
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
delete(scene, "brazil")
for k, v := range scene {
fmt.Println(k, v)
}
//route 66
//china 960
type SafeMap struct {
Data map[string]interface{}
Lock sync.RWMutex
}
func (this *SafeMap) Get(k string) interface{} {
this.Lock.RLock()
defer this.Lock.RUnlock()
if v, exit := this.Data[k]; exit {
return v
}
return nil
}
func (this *SafeMap) Set(k string, v interface{}) {
this.Lock.Lock()
defer this.Lock.Unlock()
if this.Data == nil {
this.Data = make(map[string]interface{})
}
this.Data[k] = v
}
var test sync.Map
//设置元素
func set (k,v interface{}){
test.Store(k,v)
}
//获得元素
func get (k interface{}) interface{}{
tem ,exit := test.Load(k)
if exit {
return tem
}
return nil
}
//传入一个 函数 ,sync.map 会内部迭代 ,运行这个函数
func ranggfunc (funcs func(key, value interface{}) bool) {
test.Range(funcs)
}
//删除元素
func del(key interface{}){
test.Delete(key)
}
if reflect.DeepEqual(deviceModel, models.DeviceModel{}) {
//code
}
//使用反射转换的效率要高于 struct->json->map
//传变量
func Struct2Map(obj interface{}) map[string]interface{} {
t := reflect.TypeOf(obj)
v := reflect.ValueOf(obj)
var data = make(map[string]interface{})
for i := 0; i < t.NumField(); i++ {
data[t.Field(i).Name] = v.Field(i).Interface()
}
return data
}
//传指针,如果一定要声明称指针( obj := new(Test)或obj := &Test{} )时,Struct2Map方法中需要将取出指针的内容然后继续操作,因为指针是没有Field()方法的。
func Struct2Map(obj interface{}) map[string]interface{} {
obj_v := reflect.ValueOf(obj)
v := obj_v.Elem()
typeOfType := v.Type()
var data = make(map[string]interface{})
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
data[typeOfType.Field(i).Name] = field.Interface()
}
return data
}
// Clone deep-copies a to b
func Clone(a, b interface{}) {
buff := new(bytes.Buffer)
enc := gob.NewEncoder(buff)
dec := gob.NewDecoder(buff)
enc.Encode(a)
dec.Decode(b)
}
func main() {
a1 := A{
AA: "jilao",
BB: 1,
}
var a2 A
Clone(a1, a2)
a2.AA = "lakjg;odfig"
fmt.Println(a1.AA)
fmt.Println(a2.AA)
}
import (
"fmt"
"log"
"math"
"strconv"
"strings"
)
// Decimal to binary 十进制转二进制
func DecBin(n int64) string {
if n < 0 {
log.Println("Decimal to binary error: the argument must be greater than zero.")
return ""
}
if n == 0 {
return "0"
}
s := ""
for q := n; q > 0; q = q / 2 {
m := q % 2
s = fmt.Sprintf("%v%v", m, s)
}
return s
}
// Decimal to octal 十进制转八进制
func DecOct(d int64) int64 {
if d == 0 {
return 0
}
if d < 0 {
log.Println("Decimal to octal error: the argument must be greater than zero.")
return -1
}
s := ""
for q := d; q > 0; q = q / 8 {
m := q % 8
s = fmt.Sprintf("%v%v", m, s)
}
n, err := strconv.Atoi(s)
if err != nil {
log.Println("Decimal to octal error:", err.Error())
return -1
}
return int64(n)
}
// Decimal to hexadecimal 十进制转16进制
func DecHex(n int64) string {
if n < 0 {
log.Println("Decimal to hexadecimal error: the argument must be greater than zero.")
return ""
}
if n == 0 {
return "0"
}
hex := map[int64]int64{10: 65, 11: 66, 12: 67, 13: 68, 14: 69, 15: 70}
s := ""
for q := n; q > 0; q = q / 16 {
m := q % 16
if m > 9 && m < 16 {
m = hex[m]
s = fmt.Sprintf("%v%v", string(m), s)
continue
}
s = fmt.Sprintf("%v%v", m, s)
}
return s
}
// Binary to decimal 二进制转十进制
func BinDec(b string) (n int64) {
s := strings.Split(b, "")
l := len(s)
i := 0
d := float64(0)
for i = 0; i < l; i++ {
f, err := strconv.ParseFloat(s[i], 10)
if err != nil {
log.Println("Binary to decimal error:", err.Error())
return -1
}
d += f * math.Pow(2, float64(l-i-1))
}
return int64(d)
}
// Octal to decimal 八进制转十进制
func OctDec(o int64) (n int64) {
s := strings.Split(strconv.Itoa(int(o)), "")
l := len(s)
i := 0
d := float64(0)
for i = 0; i < l; i++ {
f, err := strconv.ParseFloat(s[i], 10)
if err != nil {
log.Println("Octal to decimal error:", err.Error())
return -1
}
d += f * math.Pow(8, float64(l-i-1))
}
return int64(d)
}
// Hexadecimal to decimal 十六进制转十进制
func HexDec(h string) (n int64) {
s := strings.Split(strings.ToUpper(h), "")
l := len(s)
i := 0
d := float64(0)
hex := map[string]string{"A": "10", "B": "11", "C": "12", "D": "13", "E": "14", "F": "15"}
for i = 0; i < l; i++ {
c := s[i]
if v, ok := hex[c]; ok {
c = v
}
f, err := strconv.ParseFloat(c, 10)
if err != nil {
log.Println("Hexadecimal to decimal error:", err.Error())
return -1
}
d += f * math.Pow(16, float64(l-i-1))
}
return int64(d)
}
// Octal to binary 八进制转二进制
func OctBin(o int64) string {
d := OctDec(o)
if d == -1 {
return ""
}
return DecBin(d)
}
// Hexadecimal to binary 十六进制转二进制
func HexBin(h string) string {
d := HexDec(h)
if d == -1 {
return ""
}
return DecBin(d)
}
// Binary to octal 二进制转八进制
func BinOct(b string) int64 {
d := BinDec(b)
if d == -1 {
return -1
}
return DecOct(d)
}
// Binary to hexadecimal 二进制转十六进制
func BinHex(b string) string {
d := BinDec(b)
if d == -1 {
return ""
}
return DecHex(d)
}
var b2m_map map[byte]uint64 = map[byte]uint64{
0x00: 0,
0x01: 1,
0x02: 2,
0x03: 3,
0x04: 4,
0x05: 5,
0x06: 6,
0x07: 7,
0x08: 8,
0x09: 9,
0x0A: 10,
0x0B: 11,
0x0C: 12,
0x0D: 13,
0x0E: 14,
0x0F: 15,
0x10: 16,
0x11: 17,
0x12: 18,
0x13: 19,
0x14: 20,
0x15: 21,
0x16: 22,
0x17: 23,
0x18: 24,
0x19: 25,
0x1A: 26,
0x1B: 27,
0x1C: 28,
0x1D: 29,
0x1E: 30,
0x1F: 31,
0x20: 32,
0x21: 33,
0x22: 34,
0x23: 35,
0x24: 36,
0x25: 37,
0x26: 38,
0x27: 39,
0x28: 40,
0x29: 41,
0x2A: 42,
0x2B: 43,
0x2C: 44,
0x2D: 45,
0x2E: 46,
0x2F: 47,
0x30: 48,
0x31: 49,
0x32: 50,
0x33: 51,
0x34: 52,
0x35: 53,
0x36: 54,
0x37: 55,
0x38: 56,
0x39: 57,
0x3A: 58,
0x3B: 59,
0x3C: 60,
0x3D: 61,
0x3E: 62,
0x3F: 63,
0x40: 64,
0x41: 65,
0x42: 66,
0x43: 67,
0x44: 68,
0x45: 69,
0x46: 70,
0x47: 71,
0x48: 72,
0x49: 73,
0x4A: 74,
0x4B: 75,
0x4C: 76,
0x4D: 77,
0x4E: 78,
0x4F: 79,
0x50: 80,
0x51: 81,
0x52: 82,
0x53: 83,
0x54: 84,
0x55: 85,
0x56: 86,
0x57: 87,
0x58: 88,
0x59: 89,
0x5A: 90,
0x5B: 91,
0x5C: 92,
0x5D: 93,
0x5E: 94,
0x5F: 95,
0x60: 96,
0x61: 97,
0x62: 98,
0x63: 99,
0x64: 100,
0x65: 101,
0x66: 102,
0x67: 103,
0x68: 104,
0x69: 105,
0x6A: 106,
0x6B: 107,
0x6C: 108,
0x6D: 109,
0x6E: 110,
0x6F: 111,
0x70: 112,
0x71: 113,
0x72: 114,
0x73: 115,
0x74: 116,
0x75: 117,
0x76: 118,
0x77: 119,
0x78: 120,
0x79: 121,
0x7A: 122,
0x7B: 123,
0x7C: 124,
0x7D: 125,
0x7E: 126,
0x7F: 127,
0x80: 128,
0x81: 129,
0x82: 130,
0x83: 131,
0x84: 132,
0x85: 133,
0x86: 134,
0x87: 135,
0x88: 136,
0x89: 137,
0x8A: 138,
0x8B: 139,
0x8C: 140,
0x8D: 141,
0x8E: 142,
0x8F: 143,
0x90: 144,
0x91: 145,
0x92: 146,
0x93: 147,
0x94: 148,
0x95: 149,
0x96: 150,
0x97: 151,
0x98: 152,
0x99: 153,
0x9A: 154,
0x9B: 155,
0x9C: 156,
0x9D: 157,
0x9E: 158,
0x9F: 159,
0xA0: 160,
0xA1: 161,
0xA2: 162,
0xA3: 163,
0xA4: 164,
0xA5: 165,
0xA6: 166,
0xA7: 167,
0xA8: 168,
0xA9: 169,
0xAA: 170,
0xAB: 171,
0xAC: 172,
0xAD: 173,
0xAE: 174,
0xAF: 175,
0xB0: 176,
0xB1: 177,
0xB2: 178,
0xB3: 179,
0xB4: 180,
0xB5: 181,
0xB6: 182,
0xB7: 183,
0xB8: 184,
0xB9: 185,
0xBA: 186,
0xBB: 187,
0xBC: 188,
0xBD: 189,
0xBE: 190,
0xBF: 191,
0xC0: 192,
0xC1: 193,
0xC2: 194,
0xC3: 195,
0xC4: 196,
0xC5: 197,
0xC6: 198,
0xC7: 199,
0xC8: 200,
0xC9: 201,
0xCA: 202,
0xCB: 203,
0xCC: 204,
0xCD: 205,
0xCE: 206,
0xCF: 207,
0xD0: 208,
0xD1: 209,
0xD2: 210,
0xD3: 211,
0xD4: 212,
0xD5: 213,
0xD6: 214,
0xD7: 215,
0xD8: 216,
0xD9: 217,
0xDA: 218,
0xDB: 219,
0xDC: 220,
0xDD: 221,
0xDE: 222,
0xDF: 223,
0xE0: 224,
0xE1: 225,
0xE2: 226,
0xE3: 227,
0xE4: 228,
0xE5: 229,
0xE6: 230,
0xE7: 231,
0xE8: 232,
0xE9: 233,
0xEA: 234,
0xEB: 235,
0xEC: 236,
0xED: 237,
0xEE: 238,
0xEF: 239,
0xF0: 240,
0xF1: 241,
0xF2: 242,
0xF3: 243,
0xF4: 244,
0xF5: 245,
0xF6: 246,
0xF7: 247,
0xF8: 248,
0xF9: 249,
0xFA: 250,
0xFB: 251,
0xFC: 252,
0xFD: 253,
0xFE: 254,
0xFF: 255,
}
func hex2int(hexB *[]byte) uint64 {
var retInt uint64
hexLen := len(*hexB)
for k, v := range *hexB {
retInt += b2m_map[v] * exponent(16, uint64(2*(hexLen-k-1)))
}
return retInt
}
fmt.Printf("时间戳(秒):%v;\n", time.Now().Unix())
fmt.Printf("时间戳(纳秒):%v;\n",time.Now().UnixNano())
fmt.Printf("时间戳(毫秒):%v;\n",time.Now().UnixNano() / 1e6)
fmt.Printf("时间戳(纳秒转换为秒):%v;\n",time.Now().UnixNano() / 1e9)
对 分小时天月年 设置格式 Format(“2006-01-02 15:04:05”) —> Format(“2006-01-02 15:00:00”), 写成00就是把对应位置直接赋值成00
st,_ := time.ParseDuration("-5s")
fmt.Println("5秒前的时间:",time.Now().Add(st).Format("2006-01-02 15:04:05"))
fmt.Println("5秒前的时间:",time.Now().Add(time.Second*-5).Format("2006-01-02 15:04:05"))
st,_ = time.ParseDuration("-5m")
fmt.Println("5分前的时间:",time.Now().Add(st).Format("2006-01-02 15:04:05"))
fmt.Println("5分前的时间:",time.Now().Add(time.Minute*-5).Format("2006-01-02 15:04:05"))
st,_ = time.ParseDuration("-5h")
fmt.Println("5小时前的时间:",time.Now().Add(st).Format("2006-01-02 15:04:05"))
fmt.Println("5小时前的时间:",time.Now().Add(time.Hour*-5).Format("2006-01-02 15:04:05"))
fmt.Println("5天前的时间:",time.Now().AddDate(0, 0, -5).Format("2006-01-02 15:04:05"))
fmt.Println("5月前的时间:",time.Now().AddDate(0, -5, 0).Format("2006-01-02 15:04:05"))
fmt.Println("5年前的时间:",time.Now().AddDate(-5, 0, 0).Format("2006-01-02 15:04:05"))
package main
import (
"log"
"time"
)
func main() {
t := int64(1595581744) //外部传入的时间戳(秒为单位),必须为int64类型, time.Unix(t, 0)必须是到秒的时间戳
t1 := "2019-01-08 13:50:30" //外部传入的时间字符串
//时间转换的模板,golang里面只能是 "2006-01-02 15:04:05" (go的诞生时间)
timeTemplate1 := "2006-01-02 15:04:05" //常规类型
timeTemplate2 := "2006/01/02 15:04:05" //其他类型
timeTemplate3 := "2006-01-02" //其他类型
timeTemplate4 := "15:04:05" //其他类型
// ======= 将时间戳格式化为日期字符串 =======
log.Println(time.Unix(t, 0).Format(timeTemplate1)) //输出:2019-01-08 13:50:30
log.Println(time.Unix(t, 0).Format(timeTemplate2)) //输出:2019/01/08 13:50:30
log.Println(time.Unix(t, 0).Format(timeTemplate3)) //输出:2019-01-08
log.Println(time.Unix(t, 0).Format(timeTemplate4)) //输出:13:50:30
// ======= 将时间字符串转换为时间戳 =======
stamp, _ := time.ParseInLocation(timeTemplate1, t1, time.Local) //使用parseInLocation将字符串格式化返回本地时区时间
log.Println(stamp.Unix()) //输出:1546926630
}
i1, err := strconv.Atoi("1")
i64, err := strconv.ParseInt("2", 10, 64)
hex_str := "4161"
hex_data, _ := hex.DecodeString(hex_str)
fmt.Println(string(hexData))//Aa
向上取整math.Ceil() 向下取整math.Floor()
//go没有其他语言得round函数,整数位得四舍五入简单版 func round(x float64){ return int(math.Floor(x + 0/5)) }
package main
import (
"fmt"
"github.com/shopspring/decimal"
)
/*
保有小数位得四舍五入
*/
func main() {
v1, _ := decimal.NewFromFloat(9.824).Round(2).Float64()
v2, _ := decimal.NewFromFloat(9.826).Round(2).Float64()
v3, _ := decimal.NewFromFloat(9.8251).Round(2).Float64()
fmt.Println(v1, v2, v3)
v4, _ := decimal.NewFromFloat(9.815).Round(2).Float64()
v5, _ := decimal.NewFromFloat(9.825).Round(2).Float64()
v6, _ := decimal.NewFromFloat(9.835).Round(2).Float64()
v7, _ := decimal.NewFromFloat(9.845).Round(2).Float64()
fmt.Println(v4, v5, v6, v7)
v8, _ := decimal.NewFromFloat(3.3).Round(2).Float64()
v9, _ := decimal.NewFromFloat(3.3000000000000003).Round(2).Float64()
v10, _ := decimal.NewFromFloat(3).Round(2).Float64()
fmt.Println(v8, v9, v10)
v11, _ := decimal.NewFromFloat(129.975).Round(2).Float64()
v12, _ := decimal.NewFromFloat(34423.125).Round(2).Float64()
fmt.Println(v11, v12)
}
package main
import (
"fmt"
"log"
"os"
"path/filepath"
)
func main() {
files, err := WalkMatch("./", "*.exe")
if err != nil {
log.Fatalln(err)
}
fmt.Println(files)
}
func WalkMatch(root, pattern string) ([]string, error) {
var matches []string
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if matched, err := filepath.Match(pattern, filepath.Base(path)); err != nil {
return err
} else if matched {
path, _ = filepath.Abs(path)
matches = append(matches, path)
}
return nil
})
if err != nil {
return nil, err
}
return matches, nil
}
ctx, _ := context.WithTimeout(context.Background(), 15 * time.Second)
package main
import (
"os"
"path/filepath"
)
func main() {
// 据说这个方法在某些特别场景会获取到错误的路径
dir1, _ := os.Getwd()
//推荐使用下面的方法
dir2, _ := os.Executable()
exPath := filepath.Dir(dir2)
println(exPath2)
}
f,err = os.Open("path")
if err != nil {
driver.logger.Error("文件打开失败:"+err.Error())
os.Exit(-1)
}
dec := gob.NewDecoder(f)
err = dec.Decode(&helper.DeviceTerminalMap)
//判断有错误并且不是文件为空的错误,文件如果为空,在读文件是直接返回文件结束符(EOF)
if err != nil && err != io.EOF {
driver.logger.Error("设备终端档案存储文件解析失败:"+err.Error())
os.Exit(-1)
}
f, _ := os.Open("device-terminal.god")
defer f.Close()
dec := gob.NewDecoder(f)
err = dec.Decode(&helper.DeviceTerminalMap)
var i1 int64
i1 = 555
str1 := strconv.FormatInt(int1,10)
i2 := 1
str2 := strconv.Itoa(i2)
var int8Value int8
int8Value = 2
strconv.Itoa(int(int8Value))
//整形转换成字节
func IntToBytes(n int) []byte {
x := int32(n)
bytesBuffer := bytes.NewBuffer([]byte{})
binary.Write(bytesBuffer, binary.BigEndian, x)
return bytesBuffer.Bytes()
}
//字节转换成整形
func BytesToInt(b []byte) int {
bytesBuffer := bytes.NewBuffer(b)
var x int32
binary.Read(bytesBuffer, binary.BigEndian, &x)
return int(x)
}
package main
import (
"github.com/satori/go.uuid"
"fmt"
)
func main(){
u1 := uuid.Must(uuid.NewV4())
fmt.Printf("UUIDv4:%s\n", u1)
u2, err := uuid.FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
if err != nil {
fmt.Printf("Something went wrong: %s", err)
return
}
fmt.Printf("Successfully parsed: %s", u2)
}
//1. 按指定字符拆分
s := "iiaiibiiciiiidiiii"
sep:="ii"
arr:=strings.Split(s,sep)
fmt.Println("arr:",arr)
//2. 按空格拆分
s:=" ab cd ef gh ij kl "
arr:=strings.Fields(s)
fmt.Printf("arr:%q\n",arr)
//github.com/axgle/mahonia
func ConvertToString(src string, srcCode string, tagCode string) string {
srcCoder := mahonia.NewDecoder(srcCode)
srcResult := srcCoder.ConvertString(src)
tagCoder := mahonia.NewDecoder(tagCode)
_, cdata, _ := tagCoder.Translate([]byte(srcResult), true)
result := string(cdata)
return result
}
func main() {
//gbk编码的中文,用16进制字符串表示
hex_str := "4d6f646275732e58464a2e3330462ec0e4c4fdcbaec5c5cbaeb1c3"
hex_data, _ := hex.DecodeString(hex_str)
// 将 byte 转换 为字符串 输出结果
str := ConvertToString(string(hex_data), "gbk", "utf-8")
fmt.Println(str)
//先试试这个,一般就可以了
s1 := "4d6f646275732e58464a2e3330462ec0e4c4fdcbaec5c5cbaeb1c3"
hex_data, _ := hex.DecodeString(s1)
srcCoder := mahonia.NewDecoder("gbk")
srcResult := srcCoder.ConvertString(string(hex_data))
fmt.Println(srcResult)
}
package main
import (
"crypto/md5"
"fmt"
"io"
)
func main() {
str := "abc123"
//方法一
data := []byte(str)
has := md5.Sum(data)
md5str1 := fmt.Sprintf("%x", has) //将[]byte转成16进制
fmt.Println(md5str1)
//方法二
w := md5.New()
io.WriteString(w, str) //将str写入到w中
md5str2: = fmt.Sprintf("%x", w.Sum(nil)) //w.Sum(nil)将w的hash转成[]byte格式
fmt.Println(mdtstr2)
}
package main
import (
"encoding/base64"
"fmt"
)
func main() {
//标准base64编码
data:="abckagfd*^&&^*fadf";
sEnc:=base64.StdEncoding.EncodeToString([]byte(data))
fmt.Println(sEnc)
sDec,_:=base64.StdEncoding.DecodeString(sEnc)
fmt.Println(string(sDec))
//兼容base64编码
uEnc:=base64.URLEncoding.EncodeToString([]byte(data))
fmt.Println(uEnc)
uDec,_:=base64.URLEncoding.DecodeString(uEnc)
fmt.Println(string(uDec))
}
package main
import (
"fmt"
"reflect"
"time"
"unsafe"
)
//零拷贝字符串转字节数组
func string2bytes(s string) []byte {
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
var b []byte
pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))
pbytes.Data = stringHeader.Data
pbytes.Len = stringHeader.Len
pbytes.Cap = stringHeader.Len
return b
}
//零拷贝字节数组转字符串
func bytes2string(b []byte) string {
bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
var s string
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
stringHeader.Data = bHeader.Data
stringHeader.Len = bHeader.Len
return s
}
func main() {
s := "零拷贝转换字符串和字节数组"
t1 := time.Now().Nanosecond()
v := string2bytes(s)
t2 := time.Now().Nanosecond()
fmt.Println(v)
t3 := time.Now().Nanosecond()
data := []byte(s)
t4 := time.Now().Nanosecond()
fmt.Println(data)
fmt.Println("method1 time", t2-t1)
fmt.Println("method2 time", t4-t3)
t5 := time.Now().Nanosecond()
s1 := bytes2string(v)
t6 := time.Now().Nanosecond()
fmt.Println(s1)
t7 := time.Now().Nanosecond()
s2 := string(v)
t8 := time.Now().Nanosecond()
fmt.Println(s2)
fmt.Println("method3 time", t6-t5)
fmt.Println("method4 time", t8-t7)
}
//isSymbol表示有无符号
func BytesToInt(b []byte, isSymbol bool) (int, error){
if isSymbol {
return bytesToIntS(b)
}
return bytesToIntU(b)
}
//字节数(大端)组转成int(无符号的)
func bytesToIntU(b []byte) (int, error) {
if len(b) == 3 {
b = append([]byte{0},b...)
}
bytesBuffer := bytes.NewBuffer(b)
switch len(b) {
case 1:
var tmp uint8
err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)
return int(tmp), err
case 2:
var tmp uint16
err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)
return int(tmp), err
case 4:
var tmp uint32
err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)
return int(tmp), err
default:
return 0,fmt.Errorf("%s", "BytesToInt bytes lenth is invaild!")
}
}
//字节数(大端)组转成int(有符号)
func bytesToIntS(b []byte) (int, error) {
if len(b) == 3 {
b = append([]byte{0},b...)
}
bytesBuffer := bytes.NewBuffer(b)
switch len(b) {
case 1:
var tmp int8
err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)
return int(tmp), err
case 2:
var tmp int16
err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)
return int(tmp), err
case 4:
var tmp int32
err := binary.Read(bytesBuffer, binary.BigEndian, &tmp)
return int(tmp), err
default:
return 0,fmt.Errorf("%s", "BytesToInt bytes lenth is invaild!")
}
}
//整形转换成字节
func IntToBytes(n int,b byte) ([]byte,error) {
switch b {
case 1:
tmp := int8(n)
bytesBuffer := bytes.NewBuffer([]byte{})
binary.Write(bytesBuffer, binary.BigEndian, &tmp)
return bytesBuffer.Bytes(),nil
case 2:
tmp := int16(n)
bytesBuffer := bytes.NewBuffer([]byte{})
binary.Write(bytesBuffer, binary.BigEndian, &tmp)
return bytesBuffer.Bytes(),nil
case 3,4:
tmp := int32(n)
bytesBuffer := bytes.NewBuffer([]byte{})
binary.Write(bytesBuffer, binary.BigEndian, &tmp)
return bytesBuffer.Bytes(),nil
}
return nil,fmt.Errorf("IntToBytes b param is invaild")
}
在前一阵介绍单元测试的系列文章中,曾经简单介绍过wire依赖注入框架。但当时的wire还处于alpha阶段,不过最近wire已经发布了首个beta版,API发生了一些变化,同时也承诺除非万不得已,将不会破坏API的兼容性。在前文中,介绍了一些wire的基本概况,本篇就不再重复,感兴趣的小伙伴们可以回看一下: 搞定Go单元测试(四)—— 依赖注入框架(wire)。本篇将具体介绍wire的使用方法和一些最佳实践。
本篇中的代码的完整示例可以在这里找到:wire-examples
go get github.com/google/wire/cmd/wire
我们先通过一个简单的例子,让小伙伴们对wire有一个直观的认识。下面的例子展示了一个简易wire依赖注入示例:
$ ls
main.go wire.go
main.go
package main
import "fmt"
type Message struct {
msg string
}
type Greeter struct {
Message Message
}
type Event struct {
Greeter Greeter
}
// NewMessage Message的构造函数
func NewMessage(msg string) Message {
return Message{
msg:msg,
}
}
// NewGreeter Greeter构造函数
func NewGreeter(m Message) Greeter {
return Greeter{Message: m}
}
// NewEvent Event构造函数
func NewEvent(g Greeter) Event {
return Event{Greeter: g}
}
func (e Event) Start() {
msg := e.Greeter.Greet()
fmt.Println(msg)
}
func (g Greeter) Greet() Message {
return g.Message
}
// 使用wire前
func main() {
message := NewMessage("hello world")
greeter := NewGreeter(message)
event := NewEvent(greeter)
event.Start()
}
/*
// 使用wire后
func main() {
event := InitializeEvent("hello_world")
event.Start()
}*/
wire.go
// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package main
import "github.com/google/wire"
// InitializeEvent 声明injector的函数签名
func InitializeEvent(msg string) Event{
wire.Build(NewEvent, NewGreeter, NewMessage)
return Event{} //返回值没有实际意义,只需符合函数签名即可
}
调用wire命令生成依赖文件:
$ wire
wire: github.com/DrmagicE/wire-examples/quickstart: wrote XXXX\github.com\DrmagicE\wire-examples\quickstart\wire_gen.go
$ ls
main.go wire.go wire_gen.go
wire_gen.go wire生成的文件
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package main
// Injectors from wire.go:
func InitializeEvent(msg string) Event {
message := NewMessage(msg)
greeter := NewGreeter(message)
event := NewEvent(greeter)
return event
}
使用前 V.S 使用后
...
/*
// 使用wire前
func main() {
message := NewMessage("hello world")
greeter := NewGreeter(message)
event := NewEvent(greeter)
event.Start()
}*/
// 使用wire后
func main() {
event := InitializeEvent("hello_world")
event.Start()
}
...
使用wire后,只需调一个初始化方法既可得到Event了,对比使用前,不仅减少了三行代码,并且无需再关心依赖之间的初始化顺序。
示例传送门: quickstart
provider和injector是wire的两个核心概念。
provider: a function that can produce a value. These functions are ordinary Go code. injector: a function that calls providers in dependency order. With Wire, you write the injector’s signature, then Wire generates the function’s body. github.com/google/wire…
通过提供provider函数,让wire知道如何产生这些依赖对象。wire根据我们定义的injector函数签名,生成完整的injector函数,injector函数是最终我们需要的函数,它将按依赖顺序调用provider。
在quickstart的例子中,NewMessage,NewGreeter,NewEvent都是provider,wire_gen.go中的InitializeEvent函数是injector,可以看到injector通过按依赖顺序调用provider来生成我们需要的对象Event。
上述示例在wire.go中定义了injector的函数签名,注意要在文件第一行加上
// +build wireinject
...
用于告诉编译器无需编译该文件。在injector的签名定义函数中,通过调用wire.Build方法,指定用于生成依赖的provider:
// InitializeEvent 声明injector的函数签名
func InitializeEvent(msg string) Event{
wire.Build(NewEvent, NewGreeter, NewMessage) // <--- 传入provider函数
return Event{} //返回值没有实际意义,只需符合函数签名即可
}
该方法的返回值没有实际意义,只需要符合函数签名的要求即可。
quickstart示例展示了wire的基础功能,本节将介绍一些高级特性。
根据依赖倒置原则(Dependence Inversion Principle),对象应当依赖于接口,而不是直接依赖于具体实现。
抽象成接口依赖更有助于单元测试哦! 搞定Go单元测试(一)——基础原理 搞定Go单元测试(二)—— mock框架(gomock)
在quickstart的例子中的依赖均是具体实现,现在我们来看看在wire中如何处理接口依赖:
// UserService
type UserService struct {
userRepo UserRepository // <-- UserService依赖UserRepository接口
}
// UserRepository 存放User对象的数据仓库接口,比如可以是mysql,restful api ....
type UserRepository interface {
// GetUserByID 根据ID获取User, 如果找不到User返回对应错误信息
GetUserByID(id int) (*User, error)
}
// NewUserService *UserService构造函数
func NewUserService(userRepo UserRepository) *UserService {
return &UserService{
userRepo:userRepo,
}
}
// mockUserRepo 模拟一个UserRepository实现
type mockUserRepo struct {
foo string
bar int
}
// GetUserByID UserRepository接口实现
func (u *mockUserRepo) GetUserByID(id int) (*User,error){
return &User{}, nil
}
// NewMockUserRepo *mockUserRepo构造函数
func NewMockUserRepo(foo string,bar int) *mockUserRepo {
return &mockUserRepo{
foo:foo,
bar:bar,
}
}
// MockUserRepoSet 将 *mockUserRepo与UserRepository绑定
var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))
在这个例子中,UserService依赖UserRepository接口,其中mockUserRepo是UserRepository的一个实现,由于在Go的最佳实践中,更推荐返回具体实现而不是接口。所以mockUserRepo的provider函数返回的是*mockUserRepo这一具体类型。wire无法自动将具体实现与接口进行关联,我们需要显示声明它们之间的关联关系。通过wire.NewSet和wire.Bind将*mockUserRepo与UserRepository进行绑定:
// MockUserRepoSet 将 *mockUserRepo与UserRepository绑定
var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))
定义injector函数签名:
...
func InitializeUserService(foo string, bar int) *UserService{
wire.Build(NewUserService,MockUserRepoSet) // 使用MockUserRepoSet
return nil
}
...
示例传送门: binding-interfaces
在前面的例子中,我们的provider函数均只有一个返回值,但在某些情况下,provider函数可能会对入参做校验,如果参数错误,则需要返回error。wire也考虑了这种情况,provider函数可以将返回值的第二个参数设置成error:
// Config 配置
type Config struct {
// RemoteAddr 连接的远程地址
RemoteAddr string
}
// APIClient API客户端
type APIClient struct {
c Config
}
// NewAPIClient APIClient构造函数,如果入参校验失败,返回错误原因
func NewAPIClient(c Config) (*APIClient,error) { // <-- 第二个参数设置成error
if c.RemoteAddr == "" {
return nil, errors.New("没有设置远程地址")
}
return &APIClient{
c:c,
},nil
}
// Service
type Service struct {
client *APIClient
}
// NewService Service构造函数
func NewService(client *APIClient) *Service{
return &Service{
client:client,
}
}
类似的,injector函数定义的时候也需要将第二个返回值设置成error:
...
func InitializeClient(config Config) (*Service, error) { // <-- 第二个参数设置成error
wire.Build(NewService,NewAPIClient)
return nil,nil
}
...
观察一下wire生成的injector:
func InitializeClient(config Config) (*Service, error) {
apiClient, err := NewAPIClient(config)
if err != nil { // <-- 在构造依赖的顺序中如果发生错误,则会返回对应的"零值"和相应错误
return nil, err
}
service := NewService(apiClient)
return service, nil
}
在构造依赖的顺序中如果发生错误,则会返回对应的"零值"和相应错误。
示例传送门: return-error
当provider生成的对象需要一些cleanup处理,比如关闭文件,关闭数据库连接等操作时,依然可以通过设置provider的返回值来达到这样的效果:
// FileReader
type FileReader struct {
f *os.File
}
// NewFileReader *FileReader 构造函数,第二个参数是cleanup function
func NewFileReader(filePath string) (*FileReader, func(), error){
f, err := os.Open(filePath)
if err != nil {
return nil,nil,err
}
fr := &FileReader{
f:f,
}
fn := func() {
log.Println("cleanup")
fr.f.Close()
}
return fr,fn,nil
}
跟返回错误类似,将provider的第二个返回参数设置成func()用于返回cleanup function,上述例子中在第三个参数中返回了error,但这是可选的:
wire对provider的返回值个数和顺序有所规定:
- 第一个参数是需要生成的依赖对象
- 如果返回2个返回值,第二个参数必须是func()或者error
- 如果返回3个返回值,第二个参数必须是func(),第三个参数则必须是error
示例传送门: cleanup-functions
当一些provider通常是一起使用的时候,可以使用provider set将它们组织起来,以quickstart示例为模板稍作修改:
// NewMessage Message的构造函数
func NewMessage(msg string) Message {
return Message{
msg:msg,
}
}
// NewGreeter Greeter构造函数
func NewGreeter(m Message) Greeter {
return Greeter{Message: m}
}
// NewEvent Event构造函数
func NewEvent(g Greeter) Event {
return Event{Greeter: g}
}
func (e Event) Start() {
msg := e.Greeter.Greet()
fmt.Println(msg)
}
// EventSet Event通常是一起使用的一个集合,使用wire.NewSet进行组合
var EventSet = wire.NewSet(NewEvent, NewMessage, NewGreeter) // <--
上述例子中将Event和它的依赖通过wire.NewSet组合起来,作为一个整体在injector函数签名定义中使用:
func InitializeEvent(msg string) Event{
//wire.Build(NewEvent, NewGreeter, NewMessage)
wire.Build(EventSet)
return Event{}
}
这时只需将EventSet传入wire.Build即可。
示例传送门: provider-set
除了函数外,结构体也可以充当provider的角色,类似于setter注入:
type Foo int
type Bar int
func ProvideFoo() Foo {
return 1
}
func ProvideBar() Bar {
return 2
}
type FooBar struct {
MyFoo Foo
MyBar Bar
}
var Set = wire.NewSet(
ProvideFoo,
ProvideBar,
wire.Struct(new(FooBar), "MyFoo", "MyBar"))
通过wire.Struct来指定那些字段要被注入到结构体中,如果是全部字段,也可以简写成:
var Set = wire.NewSet(
ProvideFoo,
ProvideBar,
wire.Struct(new(FooBar), "*")) // * 表示注入全部字段
生成的injector函数:
func InitializeFooBar() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}
示例传送门: struct-provider
由于injector的函数中,不允许出现重复的参数类型,否则wire将无法区分这些相同的参数类型,比如:
type FooBar struct {
foo string
bar string
}
func NewFooBar(foo string, bar string) FooBar {
return FooBar{
foo: foo,
bar: bar,
}
}
injector函数签名定义:
// wire无法得知入参a,b跟FooBar.foo,FooBar.bar的对应关系
func InitializeFooBar(a string, b string) FooBar {
wire.Build(NewFooBar)
return FooBar{}
}
如果使用上面的provider来生成injector,wire会报如下错误:
provider has multiple parameters of type string
因为入参均是字符串类型,wire无法得知入参a,b跟FooBar.foo,FooBar.bar的对应关系。 所以我们使用不同的类型来避免冲突:
type Foo string
type Bar string
type FooBar struct {
foo Foo
bar Bar
}
func NewFooBar(foo Foo, bar Bar) FooBar {
return FooBar{
foo: foo,
bar: bar,
}
}
injector函数签名定义:
func InitializeFooBar(a Foo, b Bar) FooBar {
wire.Build(NewFooBar)
return FooBar{}
}
其中基础类型和通用接口类型是最容易发生冲突的类型,如果它们在provider函数中出现,最好统一新建一个别名来代替它(尽管还未发生冲突),例如:
type MySQLConnectionString string
type FileReader io.Reader
示例传送门 distinguishing-types
如果一个provider方法包含了许多依赖,可以将这些依赖放在一个options结构体中,从而避免构造函数的参数太多:
type Message string
// Options
type Options struct {
Messages []Message
Writer io.Writer
Reader io.Reader
}
type Greeter struct {
}
// NewGreeter Greeter的provider方法使用Options以避免构造函数过长
func NewGreeter(ctx context.Context, opts *Options) (*Greeter, error) {
return nil, nil
}
// GreeterSet 使用wire.Struct设置Options为provider
var GreeterSet = wire.NewSet(wire.Struct(new(Options), "*"), NewGreeter)
injector函数签名:
func InitializeGreeter(ctx context.Context, msg []Message, w io.Writer, r io.Reader) (*Greeter, error) {
wire.Build(GreeterSet)
return nil, nil
}
示例传送门 options-structs
由于wire自身的限制,injector中的变量类型不能重复,需要定义许多额外的基础类型别名。
目前wire命令还不能识别_test.go结尾文件中的provider函数,这样就意味着如果需要在测试中也使用wire来注入我们的mock对象,我们需要在常规代码中嵌入mock对象的provider,这对常规代码有侵入性,不过官方似乎也已经注意到了这个问题,感兴趣的小伙伴可以关注一下这条issue:github.com/google/wire…
one.go
package unittest
func AddOne(t int32) int32 {
return t + 1
}
func MinusOne(t int32) int32 {
return t - 1
}
func MultiAddOne(t int32) int32 {
t = MinusOne(t)
t = AddOne(t)
t = AddOne(t)
return t
}
one_test.go
package unittest
import (
"testing"
. "github.com/agiledragon/gomonkey"
. "github.com/smartystreets/goconvey/convey"
)
func TestMultiAddOne(t *testing.T) {
Convey("TestApplyFunc", t, func() {
Convey("input and output param", func() {
patches := ApplyFunc(AddOne, func(t1 int32) int32 {
return 5
}) //对函数AddOne打桩
defer patches.Reset()
patches.ApplyFunc(MinusOne, func(t1 int32) int32 {
return -2
}) //对函数MinusOne打桩
result := MultiAddOne(2) //看好了我调用的是MultiAddOne函数,而MultiAddOne函数内部调用了AddOne和MinusOne。
So(result, ShouldEqual, 3)
})
})
}
单元测试是代码质量的保证。本系列文章将一步步由浅入深展示如何在Go中做单元测试。
Go对单元测试的支持相当友好,标准包中就支持单元测试,在开始本系阅读之前,需要对标准测试包的基本用法有所了解。
现在,我们从单元测试的基本思想和原理入手,一起来看看如何基于Go提供的标准测试包来进行单元测试。
单元测试粒度是让人十分头疼的问题,特别是对于初尝单元测试的程序员。测试粒度做的太细,会耗费大量的开发以及维护时间,每改一个方法,都要改动其对应的测试方法。当发生代码重构的时候那简直就是噩梦(因为你所有的单元测试又都要写一遍了…)。 如单元测试粒度太粗,一个测试方法测试了n多方法,那么单元测试将显的非常臃肿,脱离了单元测试的本意,容易把单元测试写成集成测试。
单元测试一般不允许有任何外部依赖(文件依赖,网络依赖,数据库依赖等),我们不会在测试代码中去连接数据库,调用api等。这些外部依赖在执行测试的时候需要被模拟(mock/stub)。在测试的时候,我们使用模拟的对象来模拟真实依赖下的各种行为。如何运用mock/stub来模拟系统真实行为算是单元测试道路上的一只拦路虎。别着急,本文会通过示例来展示如何在Go中使用mock/stub来完成单元测试。
有的时候模拟是有效的方便的。但我们要提防过度的mock/stub,因为其会导致单元测试主要在测模拟对象而不是实际的系统。
在受益于单元测试的好处的同时,也必然增加了代码量以及维护成本(单元测试代码也是要维护的)。下面这张成本/价值象限图很清晰的阐述了在不同性质的系统中单元测试成本和价值之间的关系。

对于外部依赖少,代码又简单的代码。自然其成本和价值都是比较低的。举Go官方库里errors包为例,整个包就两个方法 New()和 Error(),没有任何外部依赖,代码也很简单,所以其单元测试起来也是相当方便。
依赖一多,mock和stub就必然增多,单元测试的成本也就随之增加。但代码又如此简单(比如上述errors包的例子),这个时候写单元测试的成本已经大于其价值,还不如不写单元测试。
像这一类代码,是最有价值写单元测试的。比如一些独立的复杂算法(银行利息计算,保险费率计算,TCP协议解析等),像这一类代码外部依赖很少,但却很容易出错,如果没有单元测试,几乎不能保证代码质量。
这种代码显然是单元测试的噩梦。写单元测试吧,代价高昂;不写单元测试吧,风险太高。像这种代码我们尽量在设计上将其分为两部分:1.处理复杂的逻辑部分 2.处理依赖部分 然后1部分进行单元测试
识别系统中的外部依赖,普遍来说,我们遇到最常见的依赖无非下面几种:
当然,还有可能是依赖还未开发完成的功能模块。但是处理方法都是大同小异的——抽象成接口,通过mock和stub进行模拟测试。
当我们开始敲产品代码的时候,我们必然已经过初步的设计,已经了解系统中的外部依赖以及业务复杂的部分,这些部分是要优先考虑写单元测试的。在写每一个方法/结构体的时候同时思考这个方法/结构体需不需要测试?如何测试?对于什么样的方法/结构体需要测试,什么样的可以不做,除了可以从上面的成本/价值象限图中获得答案外,还可以参考以下关于单元测试粒度要做多细问题的回答:
老板为我的代码付报酬,而不是测试,所以,我对此的价值观是——测试越少越好,少到你对你的代码质量达到了某种自信(我觉得这种的自信标准应该要高于业内的标准,当然,这种自信也可能是种自大)。如果我的编码生涯中不会犯这种典型的错误(如:在构造函数中设了个错误的值),那我就不会测试它。我倾向于去对那些有意义的错误做测试,所以,我对一些比较复杂的条件逻辑会异常地小心。当在一个团队中,我会非常小心的测试那些会让团队容易出错的代码。 coolshell.cn/articles/82…
Mock(模拟)和Stub(桩)是在测试过程中,模拟外部依赖行为的两种常用的技术手段。 通过Mock和Stub我们不仅可以让测试环境没有外部依赖,而且还可以模拟一些异常行为,如数据库服务不可用,没有文件的访问权限等等。
在Go语言中,可以这样描述Mock和Stub:
interface{}还是有点抽象,下面举例说明。
Mock:在测试包中创建一个结构体,满足某个外部依赖的接口 interface{}
生产代码:
//auth.go
//假设我们有一个依赖http请求的鉴权接口
type AuthService interface{
Login(username string,password string) (token string,e error)
Logout(token string) error
}
mock代码:
//auth_test.go
type authService struct {}
func (auth *authService) Login (username string,password string) (string,error){
return "token", nil
}
func (auth *authService) Logout(token string) error{
return nil
}
在这里我们用 authService实现了 AuthService接口,这样测试 Login,Logout就不再需需要依赖网络请求了。而且我们也可以模拟一些错误的情况进行测试:
//auth_test.go
//模拟登录失败
type authLoginErr struct {
auth AuthService //可以使用组合的特性,Logout方法我们不关心,只用“覆盖”Login方法即可
}
func (auth *authLoginErr) Login (username string,password string) (string,error) {
return "", errors.New("用户名密码错误")
}
//模拟api服务器宕机
type authUnavailableErr struct {
}
func (auth *authUnavailableErr) Login (username string,password string) (string,error) {
return "", errors.New("api服务不可用")
}
func (auth *authUnavailableErr) Logout(token string) error{
return errors.New("api服务不可用")
}
Stub:在测试包中创建一个模拟方法,用于替换生成代码中的方法。 这是《Go语言圣经》(11.2.3)当中的一个例子: 生产代码:
//storage.go
//发送邮件
var notifyUser = func(username, msg string) { //<--将发送邮件的方法变成一个全局变量
auth := smtp.PlainAuth("", sender, password, hostname)
err := smtp.SendMail(hostname+":587", auth, sender,
[]string{username}, []byte(msg))
if err != nil {
log.Printf("smtp.SendEmail(%s) failed: %s", username, err)
}
}
//检查quota,quota不足将发邮件
func CheckQuota(username string) {
used := bytesInUse(username)
const quota = 1000000000 // 1GB
percent := 100 * used / quota
if percent < 90 {
return // OK
}
msg := fmt.Sprintf(template, used, percent)
notifyUser(username, msg) //<---发邮件
}
显然,在跑单元测试的过程中,我们肯定不会真的给用户发邮件。在书中采用了stub的方式来进行测试:
//storage_test.go
func TestCheckQuotaNotifiesUser(t *testing.T) {
var notifiedUser, notifiedMsg string
notifyUser = func(user, msg string) { //<-看这里就够了,在测试中,覆盖了发送邮件的全局变量
notifiedUser, notifiedMsg = user, msg
}
// ...simulate a 980MB-used condition...
const user = "joe@example.org"
CheckQuota(user)
if notifiedUser == "" && notifiedMsg == "" {
t.Fatalf("notifyUser not called")
}
if notifiedUser != user {
t.Errorf("wrong user (%s) notified, want %s",
notifiedUser, user)
}
const wantSubstring = "98% of your quota"
if !strings.Contains(notifiedMsg, wantSubstring) {
t.Errorf("unexpected notification message <<%s>>, "+
"want substring %q", notifiedMsg, wantSubstring)
}
}
可以看到,在Go中,如果要用stub,那将是侵入式的,必须将生产代码设计成可以用stub方法替换的形式。上述例子体现出来的结果就是:为了测试,专门用一个全局变量 notifyUser来保存了具有外部依赖的方法。然而在不提倡使用全局变量的Go语言当中,这显然是不合适的。所以,并不提倡这种Stub方式。
既然不提倡Stub方式,那是不是在Go测试当中就可以抛弃Stub了呢?原本我是这么认为的,但直到我读了这篇译文Golang 标准包布局译,虽然这篇译文讲的是包的布局,但里面的测试示例很值得学习。
//生产代码 myapp.go
package myapp
type User struct {
ID int
Name string
Address Address
}
//User的一些增删改查
type UserService interface {
User(id int) (*User, error)
Users() ([]*User, error)
CreateUser(u *User) error
DeleteUser(id int) error
}
常规Mock方式:
//测试代码 myapp_test.go
type userService struct{
}
func (u* userService) User(id int) (*User,error) {
return &User{Id:1,Name:"name",Address:"address"},nil
}
//..省略其他实现方法
//模拟user不存在
type userNotFound struct {
u UserService
}
func (u* userNotFound) User(id int) (*User,error) {
return nil,errors.New("not found")
}
//其他...
一般来说,mock结构体内部很少会放变量,针对每一个要模拟的场景(比如上面的user不存在),最政治正确的方法应该是新建一个mock结构体。这样有两个好处:
但在刚才提到的文章中,他是这么做的:
//测试代码
// UserService 代表一个myapp.UserService.的 mock实现
type UserService struct {
UserFn func(id int) (*myapp.User, error)
UserInvoked bool
UsersFn func() ([]*myapp.User, error)
UsersInvoked bool
// 其他接口方法补全..
}
// User调用mock实现, 并标记这个方法为已调用
func (s *UserService) User(id int) (*myapp.User, error) {
s.UserInvoked = true
return s.UserFn(id)
}
这里不仅实现了接口,还通过在结构体内放置与接口方法函数签名一致的方法( UserFnUsersFn...),以及 XxxInvoked是否调用标识符来追踪方法的调用情况。这种做法其实将mock与stub相结合了起来:在mock对象的内部放置了可以被测试函数替换的函数变量( UserFn UsersFn…)。我们可以在我们的测试函数中,根据测试的需要,手动更换函数实现。
//mock与stub结合的方式
func TestUserNotFound(t *testing.T) {
userNotFound := &UserService{}
userNotFound.UserFn = func(id int) (*myapp.User, error) { //<--- 设置UserFn的期望返回结果
return nil,errors.New("not found")
}
//后续业务测试代码...
if !userNotFound.UserInvoked {
t.Fatal("没有调用User()方法")
}
}
// 常规mock方式
func TestUserNotFound(t *testing.T) {
userNotFound := &userNotFound{} //<---结构体方法已经决定了返回值
//后续业务测试代码
}
通过将mock与stub结合,不仅能在测试方法中动态的更改实现,还追踪方法的调用情况,上述例子中只是追踪了方法是否被调用,实际中,如果有需要,我们也可以追踪方法的调用次数,甚至是方法的调用顺序:
type UserService struct {
UserFn func(id int) (*myapp.User, error)
UserInvoked bool
UserInvokedTime int //<--追踪调用次数
UsersFn func() ([]*myapp.User, error)
UsersInvoked bool
// 其他接口方法补全..
FnCallStack []string //<---函数名slice,追踪调用顺序
}
// User调用mock实现, 并标记这个方法为已调用
func (s *UserService) User(id int) (*myapp.User, error) {
s.UserInvoked = true
s.UserInvokedTime++ //<--调用发次数
s.FnCallStack = append(s.FnCallStack,"User") //调用顺序
return s.UserFn(id)
}
但同时,我们也会发现我们的mock结构体更复杂了,维护成本也随之增加了。两种mock风格各有各的好处,反正要记得软件工程没有银弹,合适的场景选用合适的方法就行了。 但总体而言,mock与stub相结合的这种方式的确是一种不错的测试思路,尤其是当我们需要追踪函数是否调用,调用次数,调用顺序等信息时,mock+stub将是我们的不二选择。举个例子:
//缓存依赖
type Cache interface{
Get(id int) interface{} //获取某id的缓存
Put(id int,obj interface{}) //放入缓存
}
//数据库依赖
type UserRepository interface{
//....
}
//User结构体
type User struct {
//...
}
//userservice
type UserService interface{
cache Cache
repository UserRepository
}
func (u *UserService) Get(id int) *User {
//先从缓存找,缓存找不到在去repository里面找
}
func main() {
userService := NewUserService(xxx) //注入一些外部依赖
user := userService.Get(2) //获取id = 2的user
}
现在要测试 userService.Get(id)方法的行为:
这种测试通过mock+stub结合做起来将会非常方便,作为小练习,可以尝试自己实现一下。
接口需要以依赖注入的方式注入到结构体中,这样才能为测试提供替换接口实现的可能。Why?我们先看一个反面例子,形如下面的写法是无法测试的:
type A interface {
Fun1()
}
func (f *Foo) Bar() {
a := NewInstanceOfA(...参数若干) //生成A接口的某个实现
a.Fun1() //调用接口方法
}
当你辛辛苦苦的将A接口mock出来后,却发现你根本没有办法在Bar()方法中将mock对象替换进去。下面来看看正确的写法:
type A interface {
Fun1()
}
type Foo struct {
a A // A接口
}
func (f *Foo) Bar() {
f.a.Fun1() //调用接口方法
}
// NewFoo, 通过构造函数的方式,将A接口注入
func NewFoo(a A) *Foo {
return &Foo{a: A}
}
在例子中我们使用了构造函数传参的方法来做依赖注入(当然你也可以用setter的方式做)。在测试的时候,就可以通过NewFoo()方法将我们的mock对象传递给*Foo了。
通常我们会在
main.go中进行依赖注入
长篇大论了一大堆,稍微总结一下单元测试的几个关键步骤:
main.go中使用依赖注入方式将接口注入现在,我们已经对单元测试有了一个基本的认识,如果你能完成文中的小练习,那么恭喜你,你已经理解应当如何做单元测试,并成功迈出第一步了。在下一篇文章中,将介绍gomock测试框架,提高我们的测试效率。
通过阅读上一篇文章,相信你对怎么做单元测试已经有了初步的概念,可以着手对现有的项目进行改造并开展测试了。学会了走路,我们尝试跑起来,本篇主要介绍gomock测试框架,让我们的单元测试更加有效率。
当针对某方法进行单元测试的时候,通常不止写一个测试用例,我们需要测试该方法在多种入参条件下是否都能正常工作,特别是要针对边界值进行测试。通常这个时候表格驱动测试就派上用场了——当你发现你在写测试方法的时候用上了复制粘贴,这就说明你需要考虑使用表格驱动测试来构建你的测试方法了。我们依旧来举个例子:
func TestTime(t *testing.T) {
testCases := []struct { // 设计我们的测试用例
gmt string
loc string
want string
}{
{"12:31", "Europe/Zuri", "13:31"}, // incorrect location name
{"12:31", "America/New_York", "7:31"}, // should be 07:31
{"08:08", "Australia/Sydney", "18:08"},
}
for _, tc := range testCases { // 循环执行测试用例
loc, err := time.LoadLocation(tc.loc)
if err != nil {
t.Fatalf("could not load location %q", tc.loc)
}
gmt, _ := time.Parse("15:04", tc.gmt)
if got := gmt.In(loc).Format("15:04"); got != tc.want {
t.Errorf("In(%s, %s) = %s; want %s", tc.gmt, tc.loc, got, tc.want)
}
}
}
表格驱动测试方法让我们的测试方法更加清晰和简练,减少了复制粘贴,并大大提高的测试代码的可读性。
还记得上文说单元测试也是需要维护的吗?单元测试也是代码的一部分,也应当被认真对待。记得要用表格驱动测试的方法来组织你的测试用例,同时别忘了像正式代码那样,写上相应的注释。 更多参考: github.com/golang/go/w… blog.golang.org/subtests
gomock是Google开源的golang测试框架。或者引用官方的话来说:“GoMock is a mocking framework for the Go programming language”。
上篇文章末尾介绍了mock和stub相结合的测试方法,可以感受到mock与stub结合起来功能固然强大——调用顺序检测,调用次数检测,动态控制函数的返回值等等,但同时,其带来的维护成本和复杂度缺是不可忽视的,手动维护这样一套测试代码那将是一场灾难。我们期望能用一套框架或者工具,在提供强大的测试功能的同时帮我们维护复杂的mock代码。
gomock通过mockgen命令生成包含mock对象的.go文件,其生成的mock对象具备mock+stub的强大功能,并将我们从写mock对象中解放了出来:
mockgen -destination foo_mock.go -source foo.go -package foo //mock foo.go里面所有的接口,将mock结果保存到foo_mock.go
gomock让我们既能使用mock与stub结合的强大功能,又不需要手动维护这些mock对象,岂不美哉?
在这里我们对gomock的基本功能做一个简单演示: 假设我们的接口定义在 user.go:
// user.go
package user
// User 表示一个用户
type User struct {
Name string
}
// UserRepository 用户仓库
type UserRepository interface {
// 根据用户id查询得到一个用户或是错误信息
FindOne(id int) (*User,error)
}
通过mockgen在同目录下生成mock文件user_mock.go
mockgen -source user.go -destination user_mock.go -package user
然后在该目录下新建user_test.go来写我们的测试函数,上述步骤完成之后,我们的目录结构如下:
└── user
├── user.go
├── user_mock.go
└── user_test.go
// 静态设置返回值
func TestReturn(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
repo := NewMockUserRepository(ctrl)
// 期望FindOne(1)返回张三用户
repo.EXPECT().FindOne(1).Return(&User{Name: "张三"}, nil)
// 期望FindOne(2)返回李四用户
repo.EXPECT().FindOne(2).Return(&User{Name: "李四"}, nil)
// 期望给FindOne(3)返回找不到用户的错误
repo.EXPECT().FindOne(3).Return(nil, errors.New("user not found"))
// 验证一下结果
log.Println(repo.FindOne(1)) // 这是张三
log.Println(repo.FindOne(2)) // 这是李四
log.Println(repo.FindOne(3)) // user not found
log.Println(repo.FindOne(4)) //没有设置4的返回值,却执行了调用,测试不通过
}
// 动态设置返回值
func TestReturnDynamic(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
repo := NewMockUserRepository(ctrl)
// 常用方法之一:DoAndReturn(),动态设置返回值
repo.EXPECT().FindOne(gomock.Any()).DoAndReturn(func(i int) (*User,error) {
if i == 0 {
return nil, errors.New("user not found")
}
if i < 100 {
return &User{
Name:"小于100",
}, nil
} else {
return &User{
Name:"大于等于100",
}, nil
}
})
log.Println(repo.FindOne(120))
//log.Println(repo.FindOne(66))
//log.Println(repo.FindOne(0))
}
func TestTimes(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
repo := NewMockUserRepository(ctrl)
// 默认期望调用一次
repo.EXPECT().FindOne(1).Return(&User{Name: "张三"}, nil)
// 期望调用2次
repo.EXPECT().FindOne(2).Return(&User{Name: "李四"}, nil).Times(2)
// 调用多少次可以,包括0次
repo.EXPECT().FindOne(3).Return(nil, errors.New("user not found")).AnyTimes()
// 验证一下结果
log.Println(repo.FindOne(1)) // 这是张三
log.Println(repo.FindOne(2)) // 这是李四
log.Println(repo.FindOne(2)) // FindOne(2) 需调用两次,注释本行代码将导致测试不通过
log.Println(repo.FindOne(3)) // user not found, 不限调用次数,注释掉本行也能通过测试
}
func TestOrder(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
repo := NewMockUserRepository(ctrl)
o1 := repo.EXPECT().FindOne(1).Return(&User{Name: "张三"}, nil)
o2 := repo.EXPECT().FindOne(2).Return(&User{Name: "李四"}, nil)
o3 := repo.EXPECT().FindOne(3).Return(nil, errors.New("user not found"))
gomock.InOrder(o1, o2, o3) //设置调用顺序
// 按顺序调用,验证一下结果
log.Println(repo.FindOne(1)) // 这是张三
log.Println(repo.FindOne(2)) // 这是李四
log.Println(repo.FindOne(3)) // user not found
// 如果我们调整了调用顺序,将导致测试不通过:
// log.Println(repo.FindOne(2)) // 这是李四
// log.Println(repo.FindOne(1)) // 这是张三
// log.Println(repo.FindOne(3)) // user not found
}
上面的示例只展现了gomock功能的冰山一角,在本篇中不再深入讨论,更多用法请参考文档。
更多官方示例:github.com/golang/mock…
如果你完成了上一章的小练习,尝试动手使用gomock改造一下吧!
本篇介绍了表格驱动测试与gomock测试框架。运用表格驱动测试方法不仅能使测试代码更精简易读,还能提高我们测试用例的编写能力,无形中提升了单元测试的质量。gomock的功能十分丰富,想掌握各种骚操作还是要细心阅读一下官方示例,但通常20%的常规功能也足够覆盖80%的测试场景了。
表格驱动单元测试和gomock将我们的单元测试效率与质量提升了一个档次。在下一篇文章中,将介绍 testify断言库,继续优化我们的单元测试。
在上一篇,介绍了表格驱动测试方法和gomock测试框架,大大提升了测试效率与质量。本篇将介绍在测试中引入断言(assertion),进一步提升测试效率与质量。
我们先来看看Go标准包中为什么没有断言,官方在FAQ里面回答了这个问题。
总体概括一下大意就是:“Go不提供断言,我们知道这会带来一定的不便,其主要目的是为了防止你们这些程序员在错误处理上偷懒。我们知道这是一个争论点,但是我们觉得这样很coooool~~。”所以,我们引入断言库的原因也很明显了:偷懒,引入断言能为我们提供便利——提高测试效率,增强代码可读性。
在断言库的选择上,我们似乎没有过多的选择,从start数和活跃度来看,基本上是testify一枝独秀。
没有对比就没有伤害,先来看看使用testify之前的测试方法:
func TestSomeFun(t *testing.T){
...
if v != want {
t.Fatalf("v值错误,期望值:%s,实际值:%s", want, v)
}
if err != nil {
t.Fatalf("非预期的错误:%s", err)
}
if objectA != objectB {
if objectA.field1 != objectB.field1 {
// t.Fatalf() field1值错误...bla bla bla
}
if objectA.field2 != objectB.field2 {
// t.Fatalf() field2值错误...bla bla bla
}
// 遍历object所有值... bla bla bla
}
...
}
上述代码充斥着大量if...else..判断,大段错误信息拼装(真·体力活…),运气不好碰到结构体判断要得将其遍历一遍——不直观,低效,实在是不fashion。
现在,我们使用 testify来改造一下上面的测试示例:
func TestSomeFun(t *testing.T){
a := assert.New(t)
...
a.Equal(v, want)
a.Nil(err,"如果你还是想输出自己拼装的错误信息,可以传第三个参数")
a.Equal(objectA, objectB)
...
}
三行搞定,测试含义一目了然——直观,高效,简短,fashion。
testify使用简单,提升显著,可谓是用一次就会爱上的懒人神器。在结合表格驱动测试,gomock和testify后,我们已经能写出一手优雅漂亮的单元测试代码了。不过,光测试代码优雅还不够,我们还需要帮main.go也打扮打扮。在下一篇,也是本系列最后一篇文章中,我们将介绍wire依赖注入框架,帮main.go减肥瘦身。
在第一篇文章中提到过,为了让代码可测,需要用依赖注入的方式来构建我们的对象,而通常我们会在main.go做依赖注入,这就导致main.go会越来越臃肿。为了让单元测试得以顺利进行,main.go牺牲了它本应该纤细苗条的身材。太胖的main.go可不是什么好的信号,本篇将介绍依赖注入框架(wire),致力于帮助main.go恢复身材。
在main.go中做依赖注入,意味着在初始化代码中我们要管理:
对于小型项目而言,依赖的数量比较少,初始化代码不会很多,不需要引入依赖注入框架。但对于依赖较多的中大型项目,初始化代码又臭又长,可读性和维护性变的很差,随意感受一下:
func main() {
config := NewConfig()
// db依赖配置
db, err := ConnectDatabase(config)
if err != nil {
panic(err)
}
// PersonRepository 依赖db
personRepository := NewPersonRepository(db)
// PersonService 依赖配置 和 PersonRepository
personService := NewPersonService(config, personRepository)
// NewServer 依赖配置和PersonService
server := NewServer(config, personService)
server.Run()
}
实践表明,修改有大量依赖关系的初始化代码是一项乏味且耗时的工作。这个时候,我们就需要依赖注入框架来帮忙,简化初始化代码。
wire是google开源的依赖注入框架。或者引用官方的话来说:“Wire is a code generation tool that automates connecting components using dependency injection”。
除了wire,Go的依赖注入框架还有Uber的dig和Facebook的inject,它们都是使用反射机制来实现运行时依赖注入(runtime dependency injection),而wire则是采用代码生成的方式来达到编译时依赖注入(compile-time dependency injection)。使用反射带来的性能损失倒是其次,更重要的是反射使得代码难以追踪和调试(反射会令Ctrl+左键失效…)。而wire生成的代码是符合程序员常规使用习惯的代码,十分容易理解和调试。 关于wire的优点,在官方博文上有更详细的的介绍: blog.golang.org/wire
本部分内容参考官方博文:blog.golang.org/wire
wire有两个基本的概念:provider和injector。
provider就是普通的Go函数,可以把它看作是某对象的构造函数,我们通过provider告诉wire该对象的依赖情况:
// NewUserStore是*UserStore的provider,表明*UserStore依赖于*Config和 *mysql.DB.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
// NewDefaultConfig是*Config的provider,没有依赖
func NewDefaultConfig() *Config {...}
// NewDB是*mysql.DB的provider,依赖于ConnectionInfo
func NewDB(info ConnectionInfo) (*mysql.DB, error) {...}
// UserStoreSet 可选项,可以使用wire.NewSet将通常会一起使用的依赖组合起来。
var UserStoreSet = wire.NewSet(NewUserStore, NewDefaultConfig)
injector是wire生成的函数,我们通过调用injector来获取我们所需的对象或值,injector会按照依赖关系,按顺序调用provider函数:
// File: wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
// initUserStore是由wire生成的injector
func initUserStore(info ConnectionInfo) (*UserStore, error) {
// *Config的provider函数
defaultConfig := NewDefaultConfig()
// *mysql.DB的provider函数
db, err := NewDB(info)
if err != nil {
return nil, err
}
// *UserStore的provider函数
userStore, err := NewUserStore(defaultConfig, db)
if err != nil {
return nil, err
}
return userStore, nil
}
injector帮我们把按顺序初始化依赖的步骤给做了,我们在main.go中只需要调用initUserStore方法就能得到我们想要的对象了。
那么wire是怎么知道如何生成injector的呢?我们需要写一个函数来告诉它:
wire.Build方法列举生成injector所需的provider例如:
// initUserStore用于声明injector的函数签名
func initUserStore(info ConnectionInfo) (*UserStore, error) {
// wire.Build声明要获取一个UserStore需要调用到哪些provider函数
wire.Build(UserStoreSet, NewDB)
return nil, nil // 这些返回值wire并不关心。
}
有了上面的函数,wire就可以得知如何生成injector了。wire生成injector的步骤描述如下:
func initUserStore(info ConnectionInfo) (*UserStore, error)*UserStorewire.Build列表,找到*UserStore的provider:NewUserStorefunc NewUserStore(cfg *Config, db *mysql.DB)得知NewUserStore依赖于*Config, 和*mysql.DBwire.Build列表,找到*Config和*mysql.DB的provider:NewDefaultConfig和NewDBfunc NewDefaultConfig() *Config得知*Config没有其他依赖了。func NewDB(info *ConnectionInfo) (*mysql.DB, error)得知*mysql.DB依赖于ConnectionInfo。wire.Build列表,找不到ConnectionInfo的provider,但在injector函数签名中发现匹配的入参类型,直接使用该参数作为NewDB的入参。error栗子传送门:wire-examples
截止本文发布前,官方表明wire的项目状态是alpha,还不适合到生产环境,API存在变化的可能。 虽然是alpha,但其主要作用是为我们生成依赖注入代码,其生成的代码十分通俗易懂,在做好版本控制的前提下,即使是API发生变化,也不会对生成环境造成多坏的影响。我认为还是可以放心使用的。
本篇是本系列的最后一篇,回顾前几篇文章,我们以单元测试的原理与基本思想为基础,介绍了表格驱动测试方法,gomock,testify,wire这几样实用工具,经历了“能写单元测试”到“写好单元测试”不断优化的过程。希望本系列文章能让你有所收获。
go.mod
require (
github.com/go-ole/go-ole v1.2.4 // indirect
github.com/mattn/go-adodb v0.0.1
github.com/robfig/cron/v3 v3.0.1
github.com/wonderivan/logger v1.0.0
golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
)
main.go
package main
import (
"database/sql"
"flag"
"fmt"
"log"
"strconv"
_ "github.com/mattn/go-adodb"
"github.com/robfig/cron/v3"
"github.com/wonderivan/logger"
)
var (
local bool
remoteIP string
remoteDS string
database string
)
func init() {
flag.BoolVar(&local, "local", false, "set window connect.")
flag.StringVar(&remoteIP, "remoteIP", "192.168.0.10", "set up remote mssql of ip.")
flag.StringVar(&remoteDS, "remoteDS", "MSSQLSERVER", "set up remote mssql of datasource.")
flag.StringVar(&database, "database", "drivertest", "set up remote mssql of database.")
}
type Mssql struct {
*sql.DB
dataSource string
database string
windows bool
sa *SA
}
type SA struct {
user string
passwd string
port int
}
func NewMssql() *Mssql {
mssql := new(Mssql)
dataS := "localhost"
if !local {
dataS = fmt.Sprintf("%s\\%s", remoteIP, remoteDS)
}
mssql = &Mssql{
// 如果数据库是默认实例(MSSQLSERVER)则直接使用IP,命名实例需要指明。
// dataSource: "192.168.1.104\\MSSQLSERVER",
dataSource: dataS,
database: database,
// windows: true 为windows身份验证,false 必须设置sa账号和密码
windows: local,
sa: &SA{
user: "elink",
passwd: "elink888",
port: 1433,
},
}
return mssql
}
func (m *Mssql) Open() error {
config := fmt.Sprintf("Provider=SQLOLEDB;Initial Catalog=%s;Data Source=%s",
m.database, m.dataSource)
if m.windows {
config = fmt.Sprintf("%s;Integrated Security=SSPI", config)
} else {
// sql 2000的端口写法和sql 2005以上的有所不同,在Data Source 后以逗号隔开。
config = fmt.Sprintf("%s,%d;user id=%s;password=%s",
config, m.sa.port, m.sa.user, m.sa.passwd)
}
var err error
m.DB, err = sql.Open("adodb", config)
fmt.Println(config)
return err
}
func (m *Mssql) Select() {
rows, err := m.Query("select uid, name from sysusers")
if err != nil {
fmt.Printf("select query err: %s\n", err)
}
i := 0
for rows.Next() {
var id, name string
rows.Scan(&id, &name)
fmt.Printf("id = %s, name = %s\n", id, name)
i++
}
fmt.Println(i)
}
func main() {
// flag.Parse()
// m := NewMssql()
// config1 := fmt.Sprintf("Provider=SQLOLEDB;Initial Catalog=%s;Data Source=%s",
// m.database, m.dataSource)
// if m.windows {
// config1 = fmt.Sprintf("%s;Integrated Security=SSPI", config1)
// } else {
// // sql 2000的端口写法和sql 2005以上的有所不同,在Data Source 后以逗号隔开。
// config1 = fmt.Sprintf("%s,%d;user id=%s;password=%s",
// config1, m.sa.port, m.sa.user, m.sa.passwd)
// }
// fmt.Println(config1)
// err := mssql.Open()
// checkError(err)
// mssql.Select()
// config := fmt.Sprintf("Provider=SQLOLEDB;Initial Catalog=%s;Data Source=%s\\MSSQLSERVER,%s;user id=%s;password=%s",
// "drivertest", "192.168.0.10", "1433", "elink", "elink888")
config := fmt.Sprintf("Provider=SQLOLEDB;Initial Catalog=%s;Data Source=%s\\MSSQLSERVER,%s;user id=%s;password=%s",
"znykt", "37.64.227.132", "1433", "elink", "elink888")
DB, err := sql.Open("adodb", config)
if err != nil {
log.Fatal(err)
}
crontab := cron.New(cron.WithSeconds()) //精确到秒
// 定时任务
deviceStatusOnline_spec := "*/5 * * * * ?" //秒 分 时 日 月 周
// 定义定时器调用的任务函数
_, err = crontab.AddFunc(deviceStatusOnline_spec, func() {
rows, err := DB.Query("select top 200 id from MYCARGOOUTRECORD order by id desc")
if err != nil {
fmt.Printf("select query err: %s\n", err)
}
i := 0
for rows.Next() {
// var id, name string
// rows.Scan(&id, &name)
// fmt.Printf("id = %s, name = %s\n", id, name)
i++
}
logger.Info("结果数量:" + strconv.Itoa(i))
})
if err != nil {
log.Fatal(err)
}
// 启动定时器
crontab.Start()
select {}
}
// func checkError(err error) {
// if err != nil {
// log.Fatal(err)
// }
// }
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
)
func Base64URLDecode(data string) ([]byte, error) {
var missing = (4 - len(data)%4) % 4
data += strings.Repeat("=", missing)
res, err := base64.URLEncoding.DecodeString(data)
fmt.Println(" decodebase64urlsafe is :", string(res), err)
return base64.URLEncoding.DecodeString(data)
}
func Base64UrlSafeEncode(source []byte) string {
// Base64 Url Safe is the same as Base64 but does not contain '/' and '+' (replaced by '_' and '-') and trailing '=' are removed.
bytearr := base64.StdEncoding.EncodeToString(source)
safeurl := strings.Replace(string(bytearr), "/", "_", -1)
safeurl = strings.Replace(safeurl, "+", "-", -1)
safeurl = strings.Replace(safeurl, "=", "", -1)
return safeurl
}
func AesDecrypt(crypted, key []byte) []byte {
block, err := aes.NewCipher(key)
if err != nil {
fmt.Println("err is:", err)
}
blockMode := NewECBDecrypter(block)
origData := make([]byte, len(crypted))
blockMode.CryptBlocks(origData, crypted)
origData = PKCS5UnPadding(origData)
// fmt.Println("source is :", origData, string(origData))
return origData
}
func AesEncrypt(src, key string) []byte {
block, err := aes.NewCipher([]byte(key))
if err != nil {
fmt.Println("key error1", err)
}
if src == "" {
fmt.Println("plain content empty")
}
ecb := NewECBEncrypter(block)
content := []byte(src)
content = PKCS5Padding(content, block.BlockSize())
crypted := make([]byte, len(content))
ecb.CryptBlocks(crypted, content)
// 普通base64编码加密 区别于urlsafe base64
// fmt.Println("base64 result:", base64.StdEncoding.EncodeToString(crypted))
// fmt.Println("base64UrlSafe result:", Base64UrlSafeEncode(crypted))
return crypted
}
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func PKCS5UnPadding(origData []byte) []byte {
length := len(origData)
// 去掉最后一个字节 unpadding 次
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
type ecb struct {
b cipher.Block
blockSize int
}
func newECB(b cipher.Block) *ecb {
return &ecb{
b: b,
blockSize: b.BlockSize(),
}
}
type ecbEncrypter ecb
// NewECBEncrypter returns a BlockMode which encrypts in electronic code book
// mode, using the given Block.
func NewECBEncrypter(b cipher.Block) cipher.BlockMode {
return (*ecbEncrypter)(newECB(b))
}
func (x *ecbEncrypter) BlockSize() int { return x.blockSize }
func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
if len(src)%x.blockSize != 0 {
panic("crypto/cipher: input not full blocks")
}
if len(dst) < len(src) {
panic("crypto/cipher: output smaller than input")
}
for len(src) > 0 {
x.b.Encrypt(dst, src[:x.blockSize])
src = src[x.blockSize:]
dst = dst[x.blockSize:]
}
}
type ecbDecrypter ecb
// NewECBDecrypter returns a BlockMode which decrypts in electronic code book
// mode, using the given Block.
func NewECBDecrypter(b cipher.Block) cipher.BlockMode {
return (*ecbDecrypter)(newECB(b))
}
func (x *ecbDecrypter) BlockSize() int { return x.blockSize }
func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
if len(src)%x.blockSize != 0 {
panic("crypto/cipher: input not full blocks")
}
if len(dst) < len(src) {
panic("crypto/cipher: output smaller than input")
}
for len(src) > 0 {
x.b.Decrypt(dst, src[:x.blockSize])
src = src[x.blockSize:]
dst = dst[x.blockSize:]
}
}
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
key, _ := base64.StdEncoding.DecodeString("cmVmb3JtZXJyZWZvcm1lcg==")
strkey := string(key)
fmt.Println(strkey)
fmt.Println([]byte(strkey))
fmt.Println()
fmt.Println()
fmt.Println()
// wait_encode_str := "{\"stationNo\":\"241\"}"
// encode_str := AesEncrypt(wait_encode_str, "reformerreformer")
// hex.EncodeToString(encode_str)
// fmt.Println(hex.EncodeToString(encode_str))
wait_decode_str := "34D4F1A0506840B2A6D93C20C098312FAEE02E16FC26144A0621512E9F20CBB0AD121A74EFAD7A8935B132981D6C4198E06B8949E593E2A7D6AB4AB640CC498EE58CD0A66B5B0BBAAE1DBF4C9E4A02DE5658319E93B46831595515FA9F8EFD4202631F09DADDCD575B40C7F76739B31393A455B10AE73B2093EBD6B58DB88D24ACFB0F1DD123A9883051C8986BE060A168024690058318B8271F12E9D4795ACED75BDCC4F1F9AF75743BB81066F64D934B02CD41BE0528F2CEFFC3C40AC1645CA53943024C236A75F247DF3131704BECA1AFE7ADB81343E6D5914940F4E608BECF0D290EDD3B0F0B1F9CE0CD1557479B9E888007A48B19557A989E325193BD0F3EF40C9530B6B6F0D60F231A2EA2A58E9EA0D3B212E6C7BD177D0165C3FB052CBB672E9591D57F81115EBAEF9DC8116813F8D67289A440403D3CC9C7B0F716736E04780C06FCE2BC2D01938AE55A14FD6E0965A241608A371136FC9229BB61093367C8E674AAA35C66F4004C6478C11DE085A81A8D289DDF47EFA422BB9311355FB35218112F26981084812B3FBA2CC55D1267804FE913495C1518342E94B1C45A122B0516C01488D7FB6FEB3A06F37F"
hex_data, _ := hex.DecodeString(wait_decode_str)
decode_str := AesDecrypt(hex_data, key)
fmt.Println(string(decode_str))
// b1, _ := Base64URLDecode("cmVmb3JtZXJyZWZvcm1lcg==")
// fmt.Println(string(b1))
// b2 := Base64UrlSafeEncode([]byte("reformerreformer"))
// fmt.Println(b2)
}
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
/**
* AES加密类
*
*/
public class AESUtils {
/**
* 密钥算法
*/
private static final String KEY_ALGORITHM = "AES";
private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
/**
* 初始化密钥
*
* @return byte[] 密钥
* @throws Exception
*/
public static byte[] initSecretKey() {
// 返回生成指定算法的秘密密钥的 KeyGenerator 对象
KeyGenerator kg = null;
try {
kg = KeyGenerator.getInstance(KEY_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return new byte[0];
}
// 初始化此密钥生成器,使其具有确定的密钥大小
// AES 要求密钥长度为 128
kg.init(128);
// 生成一个密钥
SecretKey secretKey = kg.generateKey();
return secretKey.getEncoded();
}
/**
* 转换密钥
*
* @param key
* 二进制密钥
* @return 密钥
*/
public static Key toKey(byte[] key) {
// 生成密钥
return new SecretKeySpec(key, KEY_ALGORITHM);
}
/**
* 加密
*
* @param data
* 待加密数据
* @param key
* 密钥
* @return byte[] 加密数据
* @throws Exception
*/
public static byte[] encrypt(byte[] data, Key key) throws Exception {
return encrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
}
/**
* 加密
*
* @param data
* 待加密数据
* @param key
* 二进制密钥
* @return byte[] 加密数据
* @throws Exception
*/
public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
return encrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
}
/**
* 加密
*
* @param data
* 待加密数据
* @param key
* 二进制密钥
* @param cipherAlgorithm
* 加密算法/工作模式/填充方式
* @return byte[] 加密数据
* @throws Exception
*/
public static byte[] encrypt(byte[] data, byte[] key, String cipherAlgorithm) throws Exception {
// 还原密钥
Key k = toKey(key);
return encrypt(data, k, cipherAlgorithm);
}
/**
* 加密
*
* @param data
* 待加密数据
* @param key
* 密钥
* @param cipherAlgorithm
* 加密算法/工作模式/填充方式
* @return byte[] 加密数据
* @throws Exception
*/
public static byte[] encrypt(byte[] data, Key key, String cipherAlgorithm) throws Exception {
// 实例化
Cipher cipher = Cipher.getInstance(cipherAlgorithm);
// 使用密钥初始化,设置为加密模式
cipher.init(Cipher.ENCRYPT_MODE, key);
// 执行操作
return cipher.doFinal(data);
}
/**
* 解密
*
* @param data
* 待解密数据
* @param key
* 二进制密钥
* @return byte[] 解密数据
* @throws Exception
*/
public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
return decrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
}
/**
* 解密
*
* @param data
* 待解密数据
* @param key
* 密钥
* @return byte[] 解密数据
* @throws Exception
*/
public static byte[] decrypt(byte[] data, Key key) throws Exception {
return decrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
}
/**
* 解密
*
* @param data
* 待解密数据
* @param key
* 二进制密钥
* @param cipherAlgorithm
* 加密算法/工作模式/填充方式
* @return byte[] 解密数据
* @throws Exception
*/
public static byte[] decrypt(byte[] data, byte[] key, String cipherAlgorithm) throws Exception {
// 还原密钥
Key k = toKey(key);
return decrypt(data, k, cipherAlgorithm);
}
/**
* 解密
*
* @param data
* 待解密数据
* @param key
* 密钥
* @param cipherAlgorithm
* 加密算法/工作模式/填充方式
* @return byte[] 解密数据
* @throws Exception
*/
public static byte[] decrypt(byte[] data, Key key, String cipherAlgorithm) throws Exception {
// 实例化
Cipher cipher = Cipher.getInstance(cipherAlgorithm);
// 使用密钥初始化,设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, key);
// 执行操作
return cipher.doFinal(data);
}
public static String showByteArray(byte[] data) {
if (null == data) {
return null;
}
StringBuilder sb = new StringBuilder("{");
for (byte b : data) {
sb.append(b).append(",");
}
sb.deleteCharAt(sb.length() - 1);
sb.append("}");
return sb.toString();
}
/**
* 将16进制转换为二进制
*
* @param hexStr
* @return
*/
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1)
return null;
byte[] result = new byte[hexStr.length() / 2];
for (int i = 0; i < hexStr.length() / 2; i++) {
int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
/**
* 将二进制转换成16进制
*
* @param buf
* @return
*/
public static String parseByte2HexStr(byte buf[]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**
* @param str
* @param key
* @return
* @throws Exception
*/
public static String aesEncrypt(String str, String key) throws Exception {
if (str == null || key == null)
return null;
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.getBytes("utf-8"), "AES"));
byte[] bytes = cipher.doFinal(str.getBytes("utf-8"));
return new BASE64Encoder().encode(bytes);
}
public static String aesDecrypt(String str, String key) throws Exception {
if (str == null || key == null)
return null;
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes("utf-8"), "AES"));
byte[] bytes = new BASE64Decoder().decodeBuffer(str);
bytes = cipher.doFinal(bytes);
return new String(bytes, "utf-8");
}
public static void main(String[] args) throws Exception {
byte[] key = initSecretKey();
System.out.println("key:" + Base64.encodeBase64String(key));
System.out.println("key:" + showByteArray(key));
// 指定key
// String kekkk = "9iEepr1twrizIEKrs1hs2A==";
String kekkk = "cmVmb3JtZXJyZWZvcm1lcg==";
byte[] bs = Base64.decodeBase64(kekkk);
System.out.println(bs);
System.out.println("kekkk:" + showByteArray(Base64.decodeBase64(kekkk)));
Key k = toKey(Base64.decodeBase64(kekkk));
String data = "{\"requestName\":\"BeforeIn\",\"requestValue\":{\"carCode\":\"浙AD0V07\",\"inTime\":\"2016-09-29 10:06:03\",\"inChannelId\":\"4\",\"GUID\":\"1403970b-4eb2-46bc-8f2b-eeec91ddcd5f\",\"inOrOut\":\"0\"},\"Type\":\"0\"}";
// System.out.println("加密前数据: string:" + data);
// System.out.println("加密前数据: byte[]:" + showByteArray(data.getBytes()));
// System.out.println();
byte[] encryptData = encrypt(data.getBytes(), k);
String encryptStr=parseByte2HexStr(encryptData);
System.out.println("加密后数据: byte[]:" + showByteArray(encryptData));
System.out.println("加密后数据: Byte2HexStr:" + encryptStr);
System.out.println();
byte[] encryptStrByte = parseHexStr2Byte("96B742F275ECC2C77374E888CC3AD5D46CDDDAFA3BBA1AA8184D07BED0D957B250DF794666F5ACA787A8D2F20FB7D195C39E78FBDBDDD2F14B00AFC021BA306B03DB52706969E8497F91084CCA48EB81D902E3C32112F1DB07B21B45314ECFA2742F838C368C770F9C21DE07B99844A691269F83C5582A4EE21FB2C22A2168420EBC4AEEE6FB7D3B182AE5158F1F8E62F3BE3AA26C4F220E382B304F91A52A8CD823B68129409BA6059621F3EC0C94BBE8C5A9C1236C9A137536502BB6354D56");
System.out.println(encryptStrByte.length);
byte[] decryptData = decrypt(encryptStrByte, k);
System.out.println("解密后数据: byte[]:" + showByteArray(decryptData));
System.out.println("解密后数据: string:" + new String(decryptData));
}
}
java依赖
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.4</version>
</dependency>
package main
import (
"encoding/base64"
"fmt"
"strings"
)
//Base64UrlSafeEncode 需要替换掉一些字符
func Base64UrlSafeEncode(source []byte) string {
// Base64 Url Safe is the same as Base64 but does not contain '/' and '+' (replaced by '_' and '-') and trailing '=' are removed.
bytearr := base64.StdEncoding.EncodeToString(source)
safeurl := strings.Replace(string(bytearr), "/", "_", -1)
safeurl = strings.Replace(safeurl, "+", "-", -1)
safeurl = strings.Replace(safeurl, "=", "", -1)
return safeurl
}
func main() {
//标准base64编码
wait_encode_data := "reformerreformer"
wait_decode_data := "cmVmb3JtZXJyZWZvcm1lcg"
//使用标准库自带的方式加密
sEnc := base64.StdEncoding.EncodeToString([]byte(wait_encode_data))
fmt.Println(sEnc)
sDec, _ := base64.StdEncoding.DecodeString("cmVmb3JtZXJyZWZvcm1lcg==")
fmt.Println(string(sDec))
//兼容base64编码
uEnc := base64.URLEncoding.EncodeToString([]byte(wait_encode_data))
fmt.Println(uEnc)
uDec, _ := base64.URLEncoding.DecodeString(wait_decode_data)
fmt.Println(string(uDec))
}
package main
import (
"encoding/hex"
"fmt"
"github.com/fxamacker/cbor/v2"
)
func main() {
b, _ := cbor.Marshal("hello world")
fmt.Println(hex.EncodeToString(b))
cborBytes, _ := hex.DecodeString(hex.EncodeToString(b))
s := ""
cbor.Unmarshal(cborBytes, &s)
fmt.Printf("%s", s)
}
package main
import (
"fmt"
"log"
"math/rand"
"sync"
"time"
)
func main() {
//顺序执行两个协程函数
ch1 := make(chan string, 5)
go testBoringWithChannelClose("boring!", ch1)
ch2 := make(chan string, 5)
go testBoringWithChannelClose("funning!", ch2)
for b := range ch1 {
go log.Printf("You say: %s", b)
}
for b := range ch2 {
go log.Printf("You say: %s", b)
}
}
func testBoringWithChannelClose(msg string, c chan string) {
for i := 0; i < 20; i++ {
c <- fmt.Sprintf("%s %d", msg, i)
time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
}
fmt.Println(msg + "结束")
close(c)
}
package main
import (
"fmt"
"log"
"math/rand"
"sync"
"time"
)
func main() {
//限制1024个协程数执行任务
goNum1024Times()
}
// 控制1024个协程
func goNum1024Times() {
var wg sync.WaitGroup
ch := make(chan struct{}, 1024)
for i := 0; i < 20000; i++ {
wg.Add(1)
ch <- struct{}{}
go func() {
defer wg.Done()
fmt.Printf("%d\n", len(ch)) //打印通道的长度
<-ch
}()
}
wg.Wait()
}
package main
import (
"CRC15/method" //因为gomod 定义了模块名为 module CRC15 所以引用目录下的method
"encoding/hex"
"fmt"
"strings"
)
func main() {
byteArr, _ := hex.DecodeString("7F39F60116C067321223344556677889800112230100000000F700000000000000000000000000000000323030B7C2D8BB353030FABBF7D63130")
checkVal := method.UsMBCRC16(byteArr)
fmt.Println(strings.ToUpper(fmt.Sprintf("%X", checkVal)))
}
go.mod
module CRC15
go 1.13
method/crc16.go
package method
import (
"encoding/hex"
"fmt"
"strings"
)
var auchCRCLo = [256]uint8{
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,
0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,
0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,
0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,
0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,
0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,
0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,
0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,
0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,
0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,
0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,
0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,
0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,
0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,
0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,
0x43, 0x83, 0x41, 0x81, 0x80, 0x40,
}
var auchCRCHi = [256]uint8{
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
}
func UsMBCRC16(pucFrame []byte) int {
ucCRCHi := 0xFF
ucCRCLo := 0xFF
iIndex := 0
for i := 0; i < len(pucFrame); i++ {
iIndex = ucCRCLo ^ int(pucFrame[i])
ucCRCLo = ucCRCHi ^ int(auchCRCHi[iIndex])
ucCRCHi = int(auchCRCLo[iIndex])
}
return ucCRCLo<<8 | ucCRCHi
}
func TopscommCrc16(param string, check string) bool {
byteArr, _ := hex.DecodeString(param)
checkVal := UsMBCRC16(byteArr)
h := fmt.Sprintf("%X", checkVal)
if h != strings.ToUpper(check) {
return false
}
return true
}
go.mod
require (
github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec
github.com/jinzhu/gorm v1.9.14
)
package main
import (
"flag"
"fmt"
_ "github.com/denisenkom/go-mssqldb"
"github.com/jinzhu/gorm"
)
type Dt_Cardtype struct {
gorm.Model
ID string `gorm:"id"`
CardCname string `gorm:"CardCname"`
}
var (
debug = flag.Bool("debug", true, "enable debugging")
password = flag.String("password", "Elink666.", "the database password")
port *int = flag.Int("port", 1433, "the database port")
server = flag.String("server", "172.16.183.131", "the database server")
user = flag.String("user", "sa", "the database user")
)
func main() {
connString := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=carpark", *server, *user, *password, *port)
db, err := gorm.Open("mssql", connString)
if err != nil {
panic("连接数据库失败")
}
defer db.Close()
db.SingularTable(true)
db.LogMode(true)
db.CommonDB()
var dc []Dt_Cardtype
db.Find(&dc)
fmt.Println(len(dc))
// // 自动迁移模式
// db.AutoMigrate(&Product{})
// // 创建
// db.Create(&Product{Code: "L1212", Price: 1000})
// // 读取
// var product Product
// db.First(&product, 1) // 查询id为1的product
// db.First(&product, "code = ?", "L1212") // 查询code为l1212的product
// // 更新 - 更新product的price为2000
// db.Model(&product).Update("Price", 2000)
// // 删除 - 删除product
// db.Delete(&product)
}
go.mod
require (
github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec
github.com/gohouse/converter v0.0.3 // indirect
github.com/gohouse/gorose v1.0.5
github.com/gohouse/gorose/v2 v2.1.7
github.com/jinzhu/gorm v1.9.14
)
package main
import (
"flag"
"fmt"
"os"
_ "github.com/denisenkom/go-mssqldb"
gorose "github.com/gohouse/gorose/v2"
)
var err error
var engin *gorose.Engin
type DtCardtype struct {
ID int64 `gorose:"id"`
CardCname string `gorose:"CardCname"`
Mark string `gorose:"Mark"`
}
var (
debug = flag.Bool("debug", true, "enable debugging")
password = flag.String("password", "123456", "the database password")
port *int = flag.Int("port", 1433, "the database port")
server = flag.String("server", "10.10.10.53", "the database server")
user = flag.String("user", "sa", "the database user")
)
func init() {
// 全局初始化数据库,并复用
// 这里的engin需要全局保存,可以用全局变量,也可以用单例
// 配置&gorose.Config{}是单一数据库配置
// 如果配置读写分离集群,则使用&gorose.ConfigCluster{}
// mysql Dsn示例 "root:root@tcp(localhost:3306)/test?charset=utf8&parseTime=true"
connString := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=carpark", *server, *user, *password, *port)
engin, err = gorose.Open(&gorose.Config{Driver: "mssql", Dsn: connString})
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
}
func DB() gorose.IOrm {
return engin.NewOrm()
}
func main() {
var u []DtCardtype
DB().Table("Park").First()
fmt.Println(u)
}
package main
import (
"bytes"
"crypto/md5"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"strconv"
"time"
)
func main() {
// GetData()
PostMethod()
}
//这是get请求
func GetData() {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //如果需要测试自签名的证书 这里需要设置跳过证书检测 否则编译报错
client := &http.Client{Transport: tr}
resp, err := client.Get("https://192.168.7.15:8080/v1/getaction.do")
//此处需要注意经过测试,不论是否有err在此之前一定要判断resp.Body如果不为空则需要关闭,测试环境go1.16
if resp.Body != nil {
defer resp.Body.Close()
}
if err != nil {
fmt.Println("error:", err)
return
}
body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}
//这是post请求
func PostMethod() {
var rsp io.Reader
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //如果需要测试自签名的证书 这里需要设置跳过证书检测 否则编译报错
client := &http.Client{Transport: tr}
data := "cmd=123"
resp, err := client.Post("https://192.168.7.15:8080/v1/postaction.do", data, rsp)
//此处需要注意经过测试,不论是否有err在此之前一定要判断resp.Body如果不为空则需要关闭,测试环境go1.16
if resp.Body != nil {
defer resp.Body.Close()
}
if err != nil {
fmt.Println("err:", err)
} else {
body, er := ioutil.ReadAll(resp.Body)
if er != nil {
fmt.Println("err:", er)
} else {
fmt.Println(string(body))
}
}
}
func PostMethod() {
//{"carmeraId":"epark-370211-dongjiakou~~cctv-dahua~channelCode~dahua!1000003$1$0$6","carmeraDesc":"","status":"1","areaId":"001","dataTime":"2020-11-17 14:30:12"}
//{"carmeraId":"epark-370211-dongjiakou~~cctv-dahua~channelCode~dahua!1000003$1$0$7","carmeraDesc":"2F走廊西","status":"1","areaId":"001","dataTime":"2020-11-17 15:42:01"}
m3 := map[string]string{
"cameraId": "epark-370211-dongjiakou~~cctv-dahua~channelCode~dahua!1000003$1$0$7",
"cameraDesc": "2F走廊西",
"status": "1",
"areaId": "001",
"dataTime": "2020-11-17 15:42:01",
}
exportData, err := json.Marshal(m3)
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Transport: tr}
req, err := http.NewRequest(http.MethodPost, "https://eug-test.elinkit.com.cn/epmet-acs/report/video/info", bytes.NewReader(exportData))
if err != nil {
log.Fatal(err.Error)
}
times := strconv.FormatInt(time.Now().Unix(), 10)
data := []byte("c3c4029b9741f26ab4a4cbd98c2310ff" + times)
has := md5.Sum(data)
accessToken := fmt.Sprintf("%x", has)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("AccessToken", accessToken)
req.Header.Set("Timestamp", times)
response, err := client.Do(req)
//此处需要注意经过测试,不论是否有err在此之前一定要判断 response.Body 如果不为空则需要关闭,测试环境go1.16
if response.Body != nil {
defer response.Body.Close()
}
if err != nil {
fmt.Println("err:", err)
}
body, er := ioutil.ReadAll(response.Body)
if er != nil {
fmt.Println("err:", er)
}
fmt.Println(string(body))
}
package main
import (
"flag"
"fmt"
)
func main() {
// 定义几个变量,用于接收命令行的参数值
var user string
var password string
var host string
var port int
// &user 就是接收命令行中输入 -u 后面的参数值,其他同理
flag.StringVar(&user, "u", "root", "账号,默认为root")
flag.StringVar(&password, "p", "", "密码,默认为空")
flag.StringVar(&host, "h", "localhost", "主机名,默认为localhost")
flag.IntVar(&port, "P", 3306, "端口号,默认为3306")
// 解析命令行参数写入注册的flag里
flag.Parse()
// 输出结果
fmt.Printf("user:%v\npassword:%v\nhost:%v\nport:%v\n",
user, password, host, port)
}
HMAC (Hash-based Message Authentication Code)是一种基于哈希函数的消息认证码算法,用于验证数据的完整性和认证消息的发送者。HMAC结合了哈希函数和密钥,通过将密钥与消息进行哈希运算来生成消息认证码
HMAC的工作原理
HMAC使用一个密钥和一个哈希函数(如MD5、SHA-1、SHA-256等)生成一个固定长度的哈希值。具体步骤如下:
输入:消息和密钥。
处理:HMAC对输入的消息和密钥进行哈希处理,生成一个固定长度的哈希值。
输出:该哈希值即为HMAC签名
HMAC的安全性
HMAC的安全性依赖于密钥的保密性和所使用的哈希函数的抗碰撞能力。只有持有密钥的一方能够生成或验证该消息的签名,从而确保消息在传输过程中没有被篡改
HMAC的应用场景
HMAC广泛应用于各种需要确保数据完整性和认证的应用场景中,例如:
IPSec :在IPSec中,HMAC用于验证IP数据包的完整性和真实性。
SSL/TLS :在SSL/TLS协议中,HMAC用于验证通信双方的身份和数据完整性。
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
)
func GenHmacSha256(message string, secret string) string {
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(message))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func main() {
key := "VHc1CBWM0YS204rydm+wWxkQrqJSVyXAAgFRktVpOF4="
str := "/report/data/device?timestamp=123456789&appKey=9aON8USr&nonce=123456"
ret := GenHmacSha256(str, key)
fmt.Printf("ret: %s\n", ret)
}
//RdYq6cvBpeF84HO8iQErRF6Aq9XdE2fDx5w4qjDWfrg=
//RdYq6cvBpeF84HO8iQErRF6Aq9XdE2fDx5w4qjDWfrg=
//6D+MQzWc3eU4eqZkXaUQ4xmeviQPljjzs4CQmF18ldk=
//6D+MQzWc3eU4eqZkXaUQ4xmeviQPljjzs4CQmF18ldk=
package main
import (
"crypto/md5"
"fmt"
"io"
)
func main() {
str := "abcdefg"
//方法一
data := []byte(str)
has := md5.Sum(data)
md5str1 := fmt.Sprintf("%x", has) //将[]byte转成16进制
fmt.Println(md5str1)
//方法二
w := md5.New()
io.WriteString(w, str) //将str写入到w中
md5str2 := fmt.Sprintf("%x", w.Sum(nil)) //w.Sum(nil)将w的hash转成[]byte格式
fmt.Println(md5str2)
}
go.mod
require (
github.com/goburrow/modbus v0.1.0
github.com/goburrow/serial v0.1.0 // indirect
)
package main
import (
"log"
"os"
"time"
"github.com/goburrow/modbus"
)
func main() {
handler := modbus.NewTCPClientHandler("localhost:502")
handler.Timeout = 10 * time.Second
handler.SlaveId = 0 //SlaveId即modbustcp协议从0开始得第6字节称,在厂家协议中称为RTU地址无效字段,标准协议中如果是0则为广播地址
handler.Logger = log.New(os.Stdout, "test: ", log.LstdFlags)
// Connect manually so that multiple requests are handled in one connection session
err := handler.Connect()
if err != nil {
log.Fatalf("连接错误:%+v", err)
}
defer handler.Close()
// modbus.Tc
// modbus.NewClient(handler)
client := modbus.NewClient(handler)
log.Printf("客户端:%+v", client)
client.ReadHoldingRegisters(40001, 10)
res, e1 := client.ReadHoldingRegisters(40001, 2)
if e1 != nil {
log.Fatalf("E1错误:%+v", e1)
}
log.Printf("资源内容1:%+v", res)
res, _ = client.ReadHoldingRegisters(40000, 3)
log.Printf("资源内容2:%+v", res)
res, _ = client.ReadHoldingRegisters(40000, 3)
log.Printf("资源内容3:%+v", res)
res, _ = client.ReadHoldingRegisters(40000, 3)
log.Printf("资源内容4:%+v", res)
results1, err := client.ReadDiscreteInputs(15, 2)
results2, err = client.WriteMultipleRegisters(1, 2, []byte{0, 3, 0, 4})
}
package main
import (
"fmt"
"github.com/konimarti/opc"
)
func main() {
client, _ := opc.NewConnection(
"Matrikon.OPC.Simulation.1", // ProgId
[]string{"localhost"}, // OPC servers nodes
[]string{"Random.Real8", "Random.String"}, // slice of OPC tags
)
defer client.Close()
// read single tag: value, quality, timestamp
fmt.Println(client.ReadItem("Random.Real8"))
// read all added tags
fmt.Println(client.Read())
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
func newfileUploadRequest(uri string, params map[string]string, paramName, path string) (UploadFileRes, error) {
file, err := os.Open(path)
if err != nil {
return UploadFileRes{}, err
}
defer file.Close()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile(paramName, filepath.Base(path))
if err != nil {
return UploadFileRes{}, err
}
_, err = io.Copy(part, file)
for key, val := range params {
_ = writer.WriteField(key, val)
}
err = writer.Close()
if err != nil {
return UploadFileRes{}, err
}
request, err := http.NewRequest("POST", uri, body)
if err != nil {
log.Fatal(err)
return UploadFileRes{}, err
}
request.Header.Add("Content-Type", writer.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(request)
if err != nil {
log.Fatal(err)
return UploadFileRes{}, err
} else {
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
// body := &bytes.Buffer{}
// _, err := body.ReadFrom(resp.Body)
if err != nil {
log.Fatal(err)
}
// fmt.Println(resp.StatusCode)
// fmt.Println(resp.Header)
// fmt.Println(body.Bytes())
var uploadFileRes UploadFileRes
json.Unmarshal(body, &uploadFileRes)
return uploadFileRes, nil
}
return UploadFileRes{}, fmt.Errorf("lib throwe")
}
func main() {
extraParams := map[string]string{}
res, err := newfileUploadRequest("http://192.168.1.56:1502/uploadeFile", extraParams, "file", "/home/koala/project/tmp/images/1.png")
if err != nil {
log.Fatal(err)
}
fmt.Println(res)
sep := string(os.PathSeparator)
fmt.Println(sep)
}
type UploadFileRes struct {
ErrCode string `json:"errCode"`
ErrMsg string `json:"errMsg"`
Data string `json:"data"`
}
csdn_private.pem
-----BEGIN csdn_privateKey-----
MIIEpAIBAAKCAQEAv5so1PIs1jaJPJ7fOQBUAzpIqf8kZ7vCETs9sGXCs2XzqwH4
sYyzPcU9NY5CBMMtev9bW0oPyETQCsaS5HtbpgHwRPqRfbcSWZXkodUUUgSNP2ss
/74tMjw4tnx4kxW/EmsZnGJGKuyY7Tf5IlI7QD6U9nWWRKAEsKSbyd/7H3HzLxoB
xZsMbXXI3MgERzJcsmacWPnVG98i7QH0oSu+a7zSse5zSrW/RtKQj8WtAOMVZ7FP
x4fqofj6JGXot4SFBkwSgnPD2613PSPMQn6/G2dHUEJqmqtJr4j8ptP+1T6tWpy0
BVm/WhbkN9ymCOlds+SGMkyeCUs6daBJilQ9VQIDAQABAoIBADDsj2qAQ86WskgW
UO0fFlSUp0Uw7rzGBnGb7M6DzUk9eRBrOnMreAEHwe9Q2a6Zn51OYqdWq9z5JR37
Qjqw/N/QkucqC8hL3JWfXnesDro6i05sMVtD1gqDsf92nNsBrH4pdqqltUD0lL/N
kQGgeZyX3jVoJOx0532rKlRLqrWGU5acSosZqTTuCQEjBH94mA1KTvLy0CqZWyWo
yZ/OBtF61TxXa5qejJl4MJ8050UV8T7tqkckK9h4bColVS58VvnYaKST5bf33YiY
sAbE7/o8B9NW/3ogwJG6yvjXnSnICOLNUofb26zftW9DrcYGmLMnsPtm8Tj2xPLj
7zAjbekCgYEA8CZNYs72yDxJjvewvYmeWSkAO9goVBHQSSyMJcgdhiWG4IkZbaAo
1Mrvs3LBIOY1VMr2OjopzwSupNvKqOokLZ+2zcijvqDmv8AgZdh79JdaRVqqXf83
Zvun6RD1Par8vIfNIWPV/ruiM0QFcgbpd3VshAOJbqLv0QVb3Lb/f5MCgYEAzECj
uf1HKCgguRDvajG8aImES+xWdXubyA0yakS6xiBt9Pyr5sNkcum3FDIynj9/i/ke
MrK/4PtewwYwemWpccWf+EAXhQhj7bqKC5TsQr3gu9eQ+A9RVOGJ7WBJca9VY8q6
7jAWf/UlEx8WPxaV11jtvchzA1erwmMwafLGUHcCgYEAwf9PGHj0psD88z9oSVT4
1DHo/G8b9P4G8nXIKWVFZG7ATHa0UfjFw1DE3oPfPAJ8Jqlmy5bc211+77KWPmoX
G7wf4pEopgA5J8G+6kc9q1LxG4GoixJ24Px+oiqO0mhkjrBtp4GNB6Dv4NYcSAcJ
ZvU22lY5GWUKsiHQGbbDI30CgYEAx9h7GdCaXc0db1YFmrb9LJ9YpVyhn6OI4a0f
5eBHivFSBMFwhIIrd0/7xLP02OcyKcdeZ6aDnWL17gXRSwDLULlXcvNqz8xM0d6R
kRFuNUNJbyFVA5EhN9bROEPcuHIgL1q9ma3NZfd7BgGFp8a2Z5ToUKee+Oc/9BtO
1Gso5LMCgYBZKa3mZp1nmWHn7zlD+HeP/MACs34ZgaR33dGIx9j5jO8s5Gw98wJg
Rfjvuf73Yq9T7VhlJp6mZ+fDVuhrbBlI32Itrb+p7FztIx3rVBXjvvuhqOFDupYl
UttiUvkrWuoTKfbPhKZkKVDYp+fm/dd6JBa+c0+2kuwo6tm/Yr3kbw==
-----END csdn_privateKey-----
csdn_PublicKey.pem
-----BEGIN csdn_PublicKey-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv5so1PIs1jaJPJ7fOQBU
AzpIqf8kZ7vCETs9sGXCs2XzqwH4sYyzPcU9NY5CBMMtev9bW0oPyETQCsaS5Htb
pgHwRPqRfbcSWZXkodUUUgSNP2ss/74tMjw4tnx4kxW/EmsZnGJGKuyY7Tf5IlI7
QD6U9nWWRKAEsKSbyd/7H3HzLxoBxZsMbXXI3MgERzJcsmacWPnVG98i7QH0oSu+
a7zSse5zSrW/RtKQj8WtAOMVZ7FPx4fqofj6JGXot4SFBkwSgnPD2613PSPMQn6/
G2dHUEJqmqtJr4j8ptP+1T6tWpy0BVm/WhbkN9ymCOlds+SGMkyeCUs6daBJilQ9
VQIDAQAB
-----END csdn_PublicKey-----
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"os"
)
func Getkeys() {
//得到私钥
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
//通过x509标准将得到的ras私钥序列化为ASN.1 的 DER编码字符串
x509_Privatekey := x509.MarshalPKCS1PrivateKey(privateKey)
//创建一个用来保存私钥的以.pem结尾的文件
fp, _ := os.Create("csdn_private.pem")
defer fp.Close()
//将私钥字符串设置到pem格式块中
pem_block := pem.Block{
Type: "csdn_privateKey",
Bytes: x509_Privatekey,
}
//转码为pem并输出到文件中
pem.Encode(fp, &pem_block)
//处理公钥,公钥包含在私钥中
publickKey := privateKey.PublicKey
//接下来的处理方法同私钥
//通过x509标准将得到的ras私钥序列化为ASN.1 的 DER编码字符串
x509_PublicKey, _ := x509.MarshalPKIXPublicKey(&publickKey)
pem_PublickKey := pem.Block{
Type: "csdn_PublicKey",
Bytes: x509_PublicKey,
}
file, _ := os.Create("csdn_PublicKey.pem")
defer file.Close()
//转码为pem并输出到文件中
pem.Encode(file, &pem_PublickKey)
}
//使用公钥进行加密
func RSA_encrypter(path string, msg []byte) []byte {
//首先从文件中提取公钥
fp, _ := os.Open(path)
defer fp.Close()
//测量文件长度以便于保存
fileinfo, _ := fp.Stat()
buf := make([]byte, fileinfo.Size())
fp.Read(buf)
//下面的操作是与创建秘钥保存时相反的
//pem解码
block, _ := pem.Decode(buf)
//x509解码,得到一个interface类型的pub
pub, _ := x509.ParsePKIXPublicKey(block.Bytes)
//加密操作,需要将接口类型的pub进行类型断言得到公钥类型
cipherText, _ := rsa.EncryptPKCS1v15(rand.Reader, pub.(*rsa.PublicKey), msg)
return cipherText
}
//使用私钥进行解密
func RSA_decrypter(path string, cipherText []byte) []byte {
//同加密时,先将私钥从文件中取出,进行二次解码
fp, _ := os.Open(path)
defer fp.Close()
fileinfo, _ := fp.Stat()
buf := make([]byte, fileinfo.Size())
fp.Read(buf)
block, _ := pem.Decode(buf)
PrivateKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
//二次解码完毕,调用解密函数
afterDecrypter, _ := rsa.DecryptPKCS1v15(rand.Reader, PrivateKey, cipherText)
return afterDecrypter
}
func main() {
// Getkeys()
//尝试调用
msg := []byte("RSA非对称加密很棒")
ciphertext := RSA_encrypter("csdn_PublicKey.pem", msg)
//转化为十六进制方便查看结果
fmt.Println(hex.EncodeToString(ciphertext))
result := RSA_decrypter("csdn_private.pem", ciphertext)
fmt.Println(string(result))
}
package main
import (
"fmt"
)
func main() {
v1 := "123"
v2 := 123
v3 := "Have a nice day\n"
v4 := "abc"
fmt.Print(v1, v2, v3, v4)
fmt.Println()
fmt.Println(v1, v2, v3, v4)
fmt.Print(v1, " ", v2, " ", v3, " ", v4, "\n")
fmt.Printf("%s%d %s %s\n", v1, v2, v3, v4)
}
package main
import (
"io"
"os"
)
func main() {
myString := ""
arguments := os.Args
if len(arguments) == 1 {
myString = "Please give me one argument!"
} else {
myString = arguments[1]
}
io.WriteString(os.Stdout, "This is Standard output\n")
io.WriteString(os.Stderr, myString)
io.WriteString(os.Stderr, "\n")
}
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
var f *os.File
f = os.Stdin
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fmt.Println(">", scanner.Text())
}
}
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
if len(os.Args) == 1 {
fmt.Println("Please give one or more floats.")
os.Exit(1)
}
arguments := os.Args
min, _ := strconv.ParseFloat(arguments[1], 64)
max, _ := strconv.ParseFloat(arguments[1], 64)
for i := 2; i < len(arguments); i++ {
n, _ := strconv.ParseFloat(arguments[i], 64)
if n < min {
min = n
}
if n > max {
max = n
}
}
fmt.Println("Min:", min)
fmt.Println("Max:", max)
}
package main
import (
"io"
"os"
)
func main() {
myString := ""
arguments := os.Args
if len(arguments) == 1 {
myString = "Please give me one argument!"
} else {
myString = arguments[1]
}
io.WriteString(os.Stdout, myString)
io.WriteString(os.Stdout, "\n")
}
package main
import (
"context"
"encoding/hex"
"fmt"
"log"
"net"
"time"
)
func main() {
// 本质上udp不存在传统意义上的服务端和客户端, 他其实是无连接的。
// 但是实际使用中, 总要有一方去监听数据, 另一方去连接所以才有了 一个服务 一个客户
// 创建一个监听服务
// conn, err := net.Dial("udp", ":30000")
// conn, err := net.DialUDP("udp", &net.UDPAddr{
// IP: net.ParseIP("0.0.0.0"),
// Port: 45103,
// }, &net.UDPAddr{
// IP: net.ParseIP("127.0.0.1"),
// Port: 30000,
// })
// 连接已经创建的服务
conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 30000,
})
if err != nil {
log.Fatalf("连接建立错误: %s", err.Error())
return
}
defer conn.Close()
//7F06FE01993FFF3C16
//7F32F601143801001F0250000100000000000000000000000000000000323030B7C2D8BB353030FABBF7D63130381411111501893E
//7F32F60115384000200250000100000000000000000000000000000000323030B7C2D8BB353030FABBF7D631303814111115012F09
// go func() {
// 创建一个计时器
timeTickerChan := time.Tick(time.Second * 2)
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*800))
defer cancel()
for {
select {
case <-timeTickerChan:
input, err := hex.DecodeString("7F06FE01993FFF3C16")
if err != nil {
fmt.Printf("输入值解码错误:%s\n", err.Error())
continue
}
//客户端请求数据写入 conn,并传输
conn.WriteToUDP([]byte(input), &net.UDPAddr{
IP: net.ParseIP("10.10.10.4"),
Port: 8887,
})
writeCount, err := conn.Write([]byte(input))
if err != nil {
log.Printf("数据发送错误:%s\n", err.Error())
continue
}
log.Printf("输入值:%s; 发送量: %d\n", string(input), writeCount)
go func(ctx context.Context) {
//服务器端返回的数据写入空buf
buf := make([]byte, 1024)
// conn.ReadFrom()
// conn.ReadFromUDP()
cnt, serverAddr, err := conn.ReadFrom(buf)
if err != nil {
log.Printf("客户端读取数据失败 %s\n", err)
return
}
//回显服务器端回传的信息
log.Printf("服务器端回复: %s; 服务端信息:%s", hex.EncodeToString(buf[0:cnt]), serverAddr.String())
}(ctx)
select {
case <-ctx.Done():
fmt.Println("call successfully!!!")
continue
case <-time.After(time.Duration(time.Millisecond * 900)):
fmt.Println("timeout!!!")
continue
}
// input, _ = hex.DecodeString("7F32F601143801001F0250000100000000000000000000000000000000323030B7C2D8BB353030FABBF7D63130381411111501893E")
// log.Printf("输入值:%v\n", input)
// //客户端请求数据写入 conn,并传输
// conn.Write([]byte(input))
// //服务器端返回的数据写入空buf
// cnt, err = conn.Read(buf)
// if err != nil {
// log.Printf("客户端读取数据失败 %s\n", err)
// continue
// }
// //回显服务器端回传的信息
// log.Printf("服务器端回复" + hex.EncodeToString(buf[0:cnt]) + "\n")
// input, _ = hex.DecodeString("7F32F60115384000200250000100000000000000000000000000000000323030B7C2D8BB353030FABBF7D631303814111115012F09")
// log.Printf("输入值:%v\n", input)
// //客户端请求数据写入 conn,并传输
// conn.Write([]byte(input))
// //服务器端返回的数据写入空buf
// cnt, err = conn.Read(buf)
// if err != nil {
// log.Printf("客户端读取数据失败 %s\n", err)
// continue
// }
// //回显服务器端回传的信息
// log.Printf("服务器端回复" + hex.EncodeToString(buf[0:cnt]) + "\n")
}
}
// }()
}
package main
import (
"encoding/json"
"fmt"
"log"
"net/url"
"sync"
"time"
"github.com/gorilla/websocket"
)
var mutex sync.Mutex
func main() {
// u := url.URL{Scheme: "ws", Host: "8.136.204.163:8000"}
u := url.URL{Scheme: "ws", Host: "localhost:8001"}
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
log.Fatalf("%+v", err)
}
defer c.Close()
// c.SetCloseHandler(func(code int, text string) error {
// fmt.Printf("SetCloseHandler %d:%s", code, text)
// // message := websocket.FormatCloseMessage(code, "")
// // c.WriteControl(CloseMessage, message, time.Now().Add(writeWait))
// return nil
// })
// c.SetPingHandler(func(appData string) error {
// fmt.Printf("SetPingHandler %s", appData)
// return nil
// })
// c.SetPongHandler(func(appData string) error {
// fmt.Printf("SetPingHandler %s", appData)
// return nil
// })
pingParam := make(map[string]interface{})
pingParam["msgId"] = 3
pingParam["action"] = "Heartbeat"
pingData := make(map[string]interface{})
pingData["echo"] = "ping"
pingParam["data"] = pingData
param := make(map[string]interface{})
param["msgId"] = 2
param["action"] = "Login"
data := make(map[string]interface{})
data["authName"] = "taian"
data["authToken"] = "7ee193f504125d7d9e715e0252648856"
data["clientName"] = "taian123"
param["data"] = data
mutex.Lock()
c.WriteJSON(param)
mutex.Unlock()
// res := make(map[string]interface{})
// err1 := c.ReadJSON(&res)
// if err1 != nil {
// fmt.Printf("%+v\n", err1)
// log.Fatalln(err1)
// }
// bs, _ := json.Marshal(res)
// log.Printf("%s\n", string(bs))
// mutex.Lock()
// err0 := c.WriteJSON(pingParam)
// mutex.Unlock()
// if err0 != nil {
// log.Fatalf("写ping命令ERR:%+v\n", err1)
// }
// res = make(map[string]interface{})
// err1 = c.ReadJSON(&res)
// if err1 != nil {
// log.Fatalf("读ping命令ERR: %+v\n", err1)
// }
// bs, _ = json.Marshal(res)
// log.Printf("%s\n", string(bs))
go func() {
t := time.NewTicker(time.Second * 4)
defer t.Stop()
for {
<-t.C
mutex.Lock()
c.WriteJSON(pingParam)
mutex.Unlock()
}
}()
go func() {
for {
res := make(map[string]interface{})
err1 := c.ReadJSON(&res)
if err1 != nil {
fmt.Printf("%+v\n", err1)
continue
}
bs, _ := json.Marshal(res)
log.Printf("%s\n", string(bs))
}
}()
// param["action"] = "GetDevice"
// data = make(map[string]interface{})
// data["index"] = 1
// data["count"] = -1
param0 := make(map[string]interface{})
param0["msgId"] = 4
param0["action"] = "RegDeviceData"
data0 := make(map[string]interface{})
data0["names"] = []string{"S01"}
data0["count"] = 1
param0["data"] = data0
mutex.Lock()
err0 := c.WriteJSON(param0)
mutex.Unlock()
if err0 != nil {
log.Fatalf("%+v", err0)
}
param1 := make(map[string]interface{})
param1["msgId"] = 4
param1["action"] = "GetRealDeviceData"
data1 := make(map[string][]string)
data1["names"] = []string{"S01"}
param1["data"] = data1
mutex.Lock()
err1 := c.WriteJSON(param1)
mutex.Unlock()
if err1 != nil {
log.Fatalf("%+v", err1)
}
select {}
}
go.mod
require (
github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec
xorm.io/xorm v1.0.2
)
main.go
package main
import (
"flag"
"fmt"
"time"
_ "github.com/denisenkom/go-mssqldb"
"xorm.io/xorm"
)
type Park struct {
ID string `xorm:"Id"`
Name string `xorm:"Name"`
}
var (
debug = flag.Bool("debug", true, "enable debugging")
password = flag.String("password", "123456", "the database password")
port *int = flag.Int("port", 1433, "the database port")
server = flag.String("server", "10.10.10.53", "the database server")
user = flag.String("user", "sa", "the database user")
)
func main() {
connString := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=ed;encrypt=disable", *server, *user, *password, *port)
engine, err := xorm.NewEngine("mssql", connString)
//控制台打印SQL语句
if err != nil {
fmt.Println(err)
return
}
defer engine.Close()
engine.SetConnMaxLifetime(120 * time.Second)
engine.SetMaxOpenConns(16)
engine.SetMaxIdleConns(8)
err = engine.Ping()
if err != nil {
fmt.Println(err)
return
}
var sv []Park
engine.ShowSQL(true)
// err = engine.SQL("SELECT TOP 1000 id,CardCname FROM Dt_CardType order by id asc").Find(&sv)
engine.Find(&sv)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(len(sv))
}
package main
import "fmt"
func main() {
var str = "Hello北京&"
//1. 按index遍历,这种方式按照字节遍历,对于非ASCII字符会出现乱码。因为"北京"每个字占3个字节
fmt.Println("按index遍历开始")
for i := 0; i < len(str); i++ {
fmt.Printf("%d:%c--", i, str[i])
}
fmt.Println("按index遍历结束")
//2. 用for-range遍历,这种方式是按照字符遍历的,不会出现乱码,但是下标index会出现不连续的情况,它具有不确定性
fmt.Println("for-range遍历开始")
for i, ch := range str {
fmt.Printf("%d:%c--", i, ch)
}
fmt.Println("for-range遍历结束")
//3. 转rune切片遍历
fmt.Println("转rune切片遍历开始")
str2 := []rune(str)
for i := 0; i < len(str2); i++ {
fmt.Printf("%d:%c--", i, str2[i])
}
fmt.Println("\n转rune切片遍历结束")
}
package main
import (
"fmt"
"time"
)
func main() {
go func() {
// 创建一个计时器
timeTickerChan := time.Tick(time.Second * 5)
for {
fmt.Println("123")
<-timeTickerChan
}
}()
fmt.Println(112233)
//一定要阻止主线程退出定时任务才能有效
select {}
}
require github.com/robfig/cron/v3 v3.0.1
package main
import (
"fmt"
"github.com/robfig/cron/v3"
"time"
)
func main() {
// 新建一个定时任务对象
// 根据cron表达式进行时间调度,cron可以精确到秒,大部分表达式格式也是从秒开始。
//crontab := cron.New() 默认从分开始进行时间调度
crontab := cron.New(cron.WithSeconds()) //精确到秒
//定义定时器调用的任务函数
task := func() {
fmt.Println("hello world", time.Now())
}
//定时任务
spec := "*/5 * * * * ?" //cron表达式,每五秒一次
// 添加定时任务,
crontab.AddFunc(spec, task)
// 启动定时器
crontab.Start()
// 定时任务是另起协程执行的,这里使用 select 简答阻塞.实际开发中需要
// 根据实际情况进行控制
select {} //阻塞主线程停止
}
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type ShowTablesRes struct {
// 注意这里也要改数据库名
TableName string `gorm:"column:Tables_in_${database}"`
}
type CreateRes struct {
Table string
CreateTable string `gorm:"column:Create Table"`
}
func main() {
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", "${username}", "${password}", "${ip}:${port}", "${database}")
// useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=true&serverTimezone=GMT%2B8"
fmt.Println(dsn)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
var showTablesRes []ShowTablesRes
db.Raw("show tables").Scan(&showTablesRes)
// fmt.Println(&showTablesRes)
for _, v := range showTablesRes {
// fmt.Printf("%s\n", v.TableName)
var createRes []CreateRes
showTableStruct := "show create table " + v.TableName
db.Raw(showTableStruct).Scan(&createRes)
fmt.Printf("%s;\n", createRes[0].CreateTable)
}
}
go.mod
require (
github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec
github.com/go-ole/go-ole v1.2.4 // indirect
github.com/mattn/go-adodb v0.0.1 // indirect
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
)
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/denisenkom/go-mssqldb"
)
// var (
// debug = flag.Bool("debug", true, "enable debugging")
// password = flag.String("password", "123456", "the database password")
// port *int = flag.Int("port", 1433, "the database port")
// server = flag.String("server", "10.10.10.53", "the database server")
// user = flag.String("user", "sa", "the database user")
// )
func main() {
// var debug = flag.Bool("debug", false, "enable debugging")
// var password = flag.String("password", "123456", "the database password")
// var port *int = flag.Int("port", 1433, "the database port")
// var server = flag.String("server", "10.10.10.53", "the database server")
// var user = flag.String("user", "sa", "the database user")
// var database = flag.String("database", "ed", "the database name")
// if *debug {
// fmt.Printf(" password:%s\n", *password)
// fmt.Printf(" port:%d\n", *port)
// fmt.Printf(" server:%s\n", *server)
// fmt.Printf(" user:%s\n", *user)
// }
connString := fmt.Sprintf("server=%s;database=%s;user id=%s;password=%s;port=%d;encrypt=disable", "10.10.10.53", "ed", "sa", "123456", 1433)
fmt.Printf(" connString:%s\n", connString)
db, err := sql.Open("mssql", connString)
if err != nil {
log.Fatal("Open connection failed:", err.Error())
return
}
err = db.Ping()
if err != nil {
fmt.Print("PING:%s", err)
return
}
fmt.Println(1)
}
package main
import (
"fmt"
"log"
"math/rand"
"sync"
"time"
"github.com/go-redis/redis"
)
var redisdb *redis.Client
var wg sync.WaitGroup
func main() {
wg.Add(1)
go testRedisBase()
wg.Wait()
}
var map1 map[string]string
func testRedisBase() {
defer wg.Done()
//连接服务器
redisdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379", // use default Addr
Password: "", // no password set
DB: 0, // use default DB
})
//心跳
pong, err := redisdb.Ping().Result()
log.Println(pong, err) // Output: PONG <nil>
result, err := redisdb.BLPop(10*time.Second, "list_test").Result()
log.Println("result:", result, err, len(result))
ch := make(chan struct{})
map1 = make(map[string]string)
go func() {
for {
content, _ := redisdb.BLPop(10*time.Second, "list_test").Result()
if content != nil {
ch <- struct{}{}
map1[content[1]] = content[1]
}
}
}()
for {
<-ch
for k, v := range map1 {
log.Println(k, ":", v)
}
if _, ok := map1["1"]; ok {
log.Println(map1["1"])
}
delete(map1, "1")
for k, v := range map1 {
log.Println(k, ":", v)
}
}
// ExampleClient_String()
// ExampleClient_List()
// ExampleClient_Hash()
// ExampleClient_Set()
// ExampleClient_SortSet()
// ExampleClient_HyperLogLog()
// ExampleClient_CMD()
// ExampleClient_Scan()
// ExampleClient_Tx()
// ExampleClient_Script()
// ExampleClient_PubSub()
}
func ExampleClient_String() {
log.Println("ExampleClient_String")
defer log.Println("ExampleClient_String")
//kv读写
err := redisdb.Set("key", "value", 1*time.Second).Err()
log.Println(err)
//获取过期时间
tm, err := redisdb.TTL("key").Result()
log.Println(tm)
val, err := redisdb.Get("key").Result()
log.Println(val, err)
val2, err := redisdb.Get("missing_key").Result()
if err == redis.Nil {
log.Println("missing_key does not exist")
} else if err != nil {
log.Println("missing_key", val2, err)
}
//不存在才设置 过期时间 nx ex
value, err := redisdb.SetNX("counter", 0, 1*time.Second).Result()
log.Println("setnx", value, err)
//Incr
result, err := redisdb.Incr("counter").Result()
log.Println("Incr", result, err)
}
func ExampleClient_List() {
log.Println("ExampleClient_List")
defer log.Println("ExampleClient_List")
//添加
log.Println(redisdb.RPush("list_test", "message1").Err())
log.Println(redisdb.RPush("list_test", "message2").Err())
//设置
log.Println(redisdb.LSet("list_test", 2, "message set").Err())
//remove
ret, err := redisdb.LRem("list_test", 3, "message1").Result()
log.Println(ret, err)
rLen, err := redisdb.LLen("list_test").Result()
log.Println(rLen, err)
//遍历
lists, err := redisdb.LRange("list_test", 0, rLen-1).Result()
log.Println("LRange", lists, err)
//pop没有时阻塞
result, err := redisdb.BLPop(30*time.Second, "list_test").Result()
log.Println("result:", result, err, len(result))
}
func ExampleClient_Hash() {
log.Println("ExampleClient_Hash")
defer log.Println("ExampleClient_Hash")
datas := map[string]interface{}{
"name": "LI LEI",
"sex": 1,
"age": 28,
"tel": 123445578,
}
//添加
if err := redisdb.HMSet("hash_test", datas).Err(); err != nil {
log.Fatal(err)
}
//获取
rets, err := redisdb.HMGet("hash_test", "name", "sex").Result()
log.Println("rets:", rets, err)
//成员
retAll, err := redisdb.HGetAll("hash_test").Result()
log.Println("retAll", retAll, err)
//存在
bExist, err := redisdb.HExists("hash_test", "tel").Result()
log.Println(bExist, err)
bRet, err := redisdb.HSetNX("hash_test", "id", 100).Result()
log.Println(bRet, err)
//删除
log.Println(redisdb.HDel("hash_test", "age").Result())
}
func ExampleClient_Set() {
log.Println("ExampleClient_Set")
defer log.Println("ExampleClient_Set")
//添加
ret, err := redisdb.SAdd("set_test", "11", "22", "33", "44").Result()
log.Println(ret, err)
//数量
count, err := redisdb.SCard("set_test").Result()
log.Println(count, err)
//删除
ret, err = redisdb.SRem("set_test", "11", "22").Result()
log.Println(ret, err)
//成员
members, err := redisdb.SMembers("set_test").Result()
log.Println(members, err)
bret, err := redisdb.SIsMember("set_test", "33").Result()
log.Println(bret, err)
redisdb.SAdd("set_a", "11", "22", "33", "44")
redisdb.SAdd("set_b", "11", "22", "33", "55", "66", "77")
//差集
diff, err := redisdb.SDiff("set_a", "set_b").Result()
log.Println(diff, err)
//交集
inter, err := redisdb.SInter("set_a", "set_b").Result()
log.Println(inter, err)
//并集
union, err := redisdb.SUnion("set_a", "set_b").Result()
log.Println(union, err)
ret, err = redisdb.SDiffStore("set_diff", "set_a", "set_b").Result()
log.Println(ret, err)
rets, err := redisdb.SMembers("set_diff").Result()
log.Println(rets, err)
}
func ExampleClient_SortSet() {
log.Println("ExampleClient_SortSet")
defer log.Println("ExampleClient_SortSet")
addArgs := make([]redis.Z, 100)
for i := 1; i < 100; i++ {
addArgs = append(addArgs, redis.Z{Score: float64(i), Member: fmt.Sprintf("a_%d", i)})
}
//log.Println(addArgs)
Shuffle := func(slice []redis.Z) {
r := rand.New(rand.NewSource(time.Now().Unix()))
for len(slice) > 0 {
n := len(slice)
randIndex := r.Intn(n)
slice[n-1], slice[randIndex] = slice[randIndex], slice[n-1]
slice = slice[:n-1]
}
}
//随机打乱
Shuffle(addArgs)
//添加
ret, err := redisdb.ZAddNX("sortset_test", addArgs...).Result()
log.Println(ret, err)
//获取指定成员score
score, err := redisdb.ZScore("sortset_test", "a_10").Result()
log.Println(score, err)
//获取制定成员的索引
index, err := redisdb.ZRank("sortset_test", "a_50").Result()
log.Println(index, err)
count, err := redisdb.SCard("sortset_test").Result()
log.Println(count, err)
//返回有序集合指定区间内的成员
rets, err := redisdb.ZRange("sortset_test", 10, 20).Result()
log.Println(rets, err)
//返回有序集合指定区间内的成员分数从高到低
rets, err = redisdb.ZRevRange("sortset_test", 10, 20).Result()
log.Println(rets, err)
//指定分数区间的成员列表
rets, err = redisdb.ZRangeByScore("sortset_test", redis.ZRangeBy{Min: "(30", Max: "(50", Offset: 1, Count: 10}).Result()
log.Println(rets, err)
}
//用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
//每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数
func ExampleClient_HyperLogLog() {
log.Println("ExampleClient_HyperLogLog")
defer log.Println("ExampleClient_HyperLogLog")
for i := 0; i < 10000; i++ {
redisdb.PFAdd("pf_test_1", fmt.Sprintf("pfkey%d", i))
}
ret, err := redisdb.PFCount("pf_test_1").Result()
log.Println(ret, err)
for i := 0; i < 10000; i++ {
redisdb.PFAdd("pf_test_2", fmt.Sprintf("pfkey%d", i))
}
ret, err = redisdb.PFCount("pf_test_2").Result()
log.Println(ret, err)
redisdb.PFMerge("pf_test", "pf_test_2", "pf_test_1")
ret, err = redisdb.PFCount("pf_test").Result()
log.Println(ret, err)
}
func ExampleClient_PubSub() {
log.Println("ExampleClient_PubSub")
defer log.Println("ExampleClient_PubSub")
//发布订阅
pubsub := redisdb.Subscribe("subkey")
_, err := pubsub.Receive()
if err != nil {
log.Fatal("pubsub.Receive")
}
ch := pubsub.Channel()
time.AfterFunc(1*time.Second, func() {
log.Println("Publish")
err = redisdb.Publish("subkey", "test publish 1").Err()
if err != nil {
log.Fatal("redisdb.Publish", err)
}
redisdb.Publish("subkey", "test publish 2")
})
for msg := range ch {
log.Println("recv channel:", msg.Channel, msg.Pattern, msg.Payload)
}
}
func ExampleClient_CMD() {
log.Println("ExampleClient_CMD")
defer log.Println("ExampleClient_CMD")
//执行自定义redis命令
Get := func(rdb *redis.Client, key string) *redis.StringCmd {
cmd := redis.NewStringCmd("get", key)
redisdb.Process(cmd)
return cmd
}
v, err := Get(redisdb, "NewStringCmd").Result()
log.Println("NewStringCmd", v, err)
v, err = redisdb.Do("get", "redisdb.do").String()
log.Println("redisdb.Do", v, err)
}
func ExampleClient_Scan() {
log.Println("ExampleClient_Scan")
defer log.Println("ExampleClient_Scan")
//scan
for i := 1; i < 1000; i++ {
redisdb.Set(fmt.Sprintf("skey_%d", i), i, 0)
}
cusor := uint64(0)
for {
keys, retCusor, err := redisdb.Scan(cusor, "skey_*", int64(100)).Result()
log.Println(keys, cusor, err)
cusor = retCusor
if cusor == 0 {
break
}
}
}
func ExampleClient_Tx() {
pipe := redisdb.TxPipeline()
incr := pipe.Incr("tx_pipeline_counter")
pipe.Expire("tx_pipeline_counter", time.Hour)
// Execute
//
// MULTI
// INCR pipeline_counter
// EXPIRE pipeline_counts 3600
// EXEC
//
// using one rdb-server roundtrip.
_, err := pipe.Exec()
fmt.Println(incr.Val(), err)
}
func ExampleClient_Script() {
IncrByXX := redis.NewScript(`
if redis.call("GET", KEYS[1]) ~= false then
return redis.call("INCRBY", KEYS[1], ARGV[1])
end
return false
`)
n, err := IncrByXX.Run(redisdb, []string{"xx_counter"}, 2).Result()
fmt.Println(n, err)
err = redisdb.Set("xx_counter", "40", 0).Err()
if err != nil {
panic(err)
}
n, err = IncrByXX.Run(redisdb, []string{"xx_counter"}, 2).Result()
fmt.Println(n, err)
}
package main
import (
"log"
"github.com/streadway/amqp"
)
func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
}
}
func main() {
// 连接 RabbitMQ
conn, err := amqp.Dial("amqp://admin:admin@127.0.0.1:5673")
failOnError(err, "连接失败")
defer conn.Close()
// 建立一个 channel ( 其实就是TCP连接 )
ch, err := conn.Channel()
failOnError(err, "打开通道失败")
defer ch.Close()
// 创建一个名字叫 "hello" 的队列
q, err := ch.QueueDeclare(
"hello", // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "创建队列失败")
// 构建一个消息
body := "Hello World!"
msg := amqp.Publishing{
ContentType: "text/plain",
Body: []byte(body),
}
// 构建一个生产者,将消息 放入队列
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
msg)
log.Printf(" [x] Sent %s", body)
failOnError(err, "Failed to publish a message")
}
该命令可以根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,下载后自动编译,一般引用依赖用go get就可以了。 参考:
go get -u "github.com/VictoriaMetrics/fastcache"
参考: https://www.kancloud.cn/cattong/go_command_tutorial/261349
该命令用于编译我们指定的源码文件或代码包以及它们的依赖包。命令的常用标记说明如下:

编译过程输出到文件:go build -x > result 2>&1,因为go build -x 最终是将日志写到标准错误流当中。
如果只在编译特定包时需要指定参数,可以参考包名=参数列表的格式,比如go build -gcflags=‘log=-N -l’ main.go
参考: https://www.kancloud.cn/cattong/go_command_tutorial/261347
该命令用于编译并安装指定的代码包及它们的依赖包。当指定的代码包的依赖包还没有被编译和安装时,该命令会先去处理依赖包。与go build命令一样,传给go install命令的代码包参数应该以导入路径的形式提供。
并且,go build命令的绝大多数标记也都可以用于go install命令。实际上,go install命令只比go build命令多做了一件事,即:安装编译后的结果文件到指定目录。
参考:
https://www.kancloud.cn/cattong/go_command_tutorial/261348
Golang的开发团队制定了统一的官方代码风格,并且推出了gofmt工具(gofmt或go fmt)来帮助开发者格式化他们的代码到统一的风格。
gofmt是一个cli程序,会优先读取标准输入,如果传入了文件路径的话,会格式化这个文件,如果传入一个目录,会格式化目录中所有.go文件,如果不传参数,会格式化当前目录下的所有.go文件。
gofmt默认不对代码进行简化,使用-s参数可以开启简化代码功能
gofmt是一个独立的cli程序,而go中还有一个go fmt命令,go fmt命令是gofmt的简单封装。go fmt在调用gofmt时添加了-l -w参数,相当于执行了gofmt -l -w
参考:
https://blog.csdn.net/whatday/article/details/97682094
该命令用于打印Go语言的环境信息,常见的通用环境信息如下:

设置或修改环境变量值:
go env -w GOPROXY="https://goproxy.com,direct"
参考:
https://www.kancloud.cn/cattong/go_command_tutorial/261359
该命令可以运行命令源码文件,只能接受一个命令源码文件以及若干个库源码文件(必须同属于main包)作为文件参数,且不能接受测试源码文件。它在执行时会检查源码文件的类型。如果参数中有多个或者没有命令源码文件,那么go run命令就只会打印错误提示信息并退出,而不会继续执行。
在通过参数检查后,go run命令会将编译参数中的命令源码文件,并把编译后的可执行文件存放到临时工作目录中。
参考:
https://www.kancloud.cn/cattong/go_command_tutorial/261352
该命令用于对Go语言编写的程序进行测试,这种测试是以代码包为单位的,命令会自动测试每一个指定的代码包。当然,前提是指定的代码包中存在测试源码文件。
参考:
https://www.kancloud.cn/cattong/go_command_tutorial/261353
该命令会删除掉执行其它命令时产生的一些文件和目录。
参考:
https://www.kancloud.cn/cattong/go_command_tutorial/261350
该命令的作用是列出指定的代码包的信息。与其他命令相同,我们需要以代码包导入路径的方式给定代码包。被给定的代码包可以有多个。这些代码包对应的目录中必须直接保存有Go语言源码文件,其子目录中的文件不算在内。
标记-e的作用是以容错模式加载和分析指定的代码包。在这种情况下,命令程序如果在加载或分析的过程中遇到错误只会在内部记录一下,而不会直接把错误信息打印出来。
为了看到错误信息可以使用-json标记。这个标记的作用是把代码包的结构体实例用JSON的样式打印出来。-m标记可以打印出modules而不是package。
# cd yky-sys-backend/cmd/bidengine
参考:
https://www.kancloud.cn/cattong/go_command_tutorial/261354
该命令初始化并写入一个新的go.mod至当前目录中,实际上是创建一个以当前目录为根的新模块。文件go.mod必须不存在。如果可能,init会从import注释(参阅“go help importpath”)或从版本控制配置猜测模块路径。要覆盖此猜测,提供模块路径作为参数 module为当前项目名。比如:
go mod init demo
参考:
https://www.jianshu.com/p/f6d2d6db2bca
该命令确保go.mod与模块中的源代码一致。它添加构建当前模块的包和依赖所必须的任何缺少的模块,删除不提供任何有价值的包的未使用的模块。它也会添加任何缺少的条目至go.mod并删除任何不需要的条目。
参考:
https://www.jianshu.com/p/f6d2d6db2bca
该命令重置主模块的vendor目录,使其包含构建和测试所有主模块的包所需要的所有包。不包括vendor中的包的测试代码。即将GOPATH或GOROOT下载的包拷贝到项目下的vendor目录,如果不使用vendor隔离项目的依赖,则不需要使用该命令拷贝依赖。
参考:
https://www.jianshu.com/p/f6d2d6db2bca
该命令下载指定名字的模块,可为选择主模块依赖的模块匹配模式,或path@version形式的模块查询。如果download不带参数则代表是主模块的所有依赖。download的只会下载依赖,不会编译依赖,和get是有区别的。
参考:
https://www.jianshu.com/p/f6d2d6db2bca
该命令提供一个编辑go.mod的命令行接口,主要提供给工具或脚本使用。它只读取go.mod;不查找涉及模块的信息。默认情况下,edit读写主模块的go.mod文件,但也可以在标志后指定不同的目标文件。
参考:
https://www.jianshu.com/p/f6d2d6db2bca
该命令以文本形式打印模块间的依赖关系图。输出的每一行行有两个字段(通过空格分割);模块和其所有依赖中的一个。每个模块都被标记为path@version形式的字符串(除了主模块,因其没有@version后缀)。
参考:
https://www.jianshu.com/p/f6d2d6db2bca
该命令查存储在本地下载源代码缓存中的当前模块的依赖,是否自从下载之后未被修改。如果所有模块都未被修改,打印“all modules verified”。否则,报告哪个模块已经被修改并令“go mod”以非0状态退出。
参考:
https://www.jianshu.com/p/f6d2d6db2bca
该命令输出每个包或者模块的引用块,每个块以注释行“# package”或“# module”开头,给出目标包或模块。随后的行通过导入图给出路径,一个包一行。每个块之间通过一个空行分割,如果包或模块没有被主模块引用,该小节将显示单独一个带圆括号的提示信息来表明该事实。
参考:
https://www.jianshu.com/p/f6d2d6db2bca
go tool的可执行文件在GOROOT或GOPATH的pkg/tool目录。go doc cmd可以查看具体cmd的使用说明,比如:
go doc pprof
在Golang中,可以通过pprof工具对应于程序的运行时进行性能分析,包括CPU、内存、Goroutine等实时信息。
参考:
https://www.kancloud.cn/cattong/go_command_tutorial/261357
该命令可以追踪请求链路,清晰的了解整个程序的调用栈,可以通过追踪器捕获大量信息。
参考:
https://zhuanlan.zhihu.com/p/410590497
该命令可以编译Go文件生成汇编代码,-N参数表示禁止编译优化, -l表示禁止内联,-S表示打印汇编,比如
# 会生成main.o的汇编文件
该命令是一个用于检查Go语言源码中静态错误的简单工具。
go vet命令是go tool vet命令的简单封装。它会首先载入和分析指定的代码包,并把指定代码包中的所有Go语言源码文件和以“.s”结尾的文件的相对路径作为参数传递给go tool vet命令。其中,以“.s”结尾的文件是汇编语言的源码文件。如果go vet命令的参数是Go语言源码文件的路径,则会直接将这些参数传递给go tool vet命令。
参考:
https://www.kancloud.cn/cattong/go_command_tutorial/261356
该命令可以打印附于Go语言程序实体上的文档。我们可以通过把程序实体的标识符作为该命令的参数来达到查看其文档的目的。
所谓Go语言的程序实体,是指变量、常量、函数、结构体以及接口。而程序实体的标识符即是代表它们的名称。
参考:
https://www.kancloud.cn/cattong/go_command_tutorial/261351
该命令可以调用栈的地址转化为文件和行号。
Usage:
该命令可以将汇编文件编译成一个.o文件,后续这个.o文件可以用于生成.a归档文件,命令的file参数必须是汇编文件。
Usage:
每一个 Go 二进制文件内,都有一个独一无二的 Build ID,详情参考 src/cmd/go/internal/work/buildid.go 。Go Build ID 可以用以下命令来查看:
go tool buildid
参考:
https://www.anquanke.com/post/id/215419
该命令可以使我们创建能够调用C语言代码的Go语言源码文件。这使得我们可以使用Go语言代码去封装一些C语言的代码库,并提供给Go语言代码或项目使用。
参考:
https://www.kancloud.cn/cattong/go_command_tutorial/261358
该命令对单元测试过程中生成的代码覆盖率统计生成html文件,可以本地打开展示。
go test -coverprofile=a.out
覆盖度工具不仅可以记录分支是否被执行,还可以记录分支被执行了多少次。
go test -covermode=set|count|atomic: -covermode:
set: 默认模式,统计是否执行
count: 计数
atomic: count的并发安全版本,仅当需要精确统计时使用
通过go tool cover -func=count.out查看每个函数的覆盖度。
参考:
https://blog.csdn.net/xhdxhdxhd/article/details/120424848
dist工具是属于go的一个引导工具,它负责构建C程序(如Go编译器)和Go工具的初始引导副本。它也可以作为一个包罗万象用shell脚本替换以前完成的零工。通过“go tool dist”命令可以操作该工具。
go tool dist
使用go tool dist list可以输出当前安装Go版本所支持的操作系统与架构。
参考:https://blog.csdn.net/byxiaoyuonly/article/details/112492264
该命令会把指定代码包的所有Go语言源码文件中的旧版本代码修正为新版本的代码。这里所说的版本即Go语言的版本。代码包的所有Go语言源码文件不包括其子代码包(如果有的话)中的文件。修正操作包括把对旧程序调用的代码更换为对新程序调用的代码、把旧的语法更换为新的语法等等。
这个工具其实非常有用。在编程语言的升级和演进的过程中,难免会对过时的和不够优秀的语法及标准库进行改进。
参考:
https://www.kancloud.cn/cattong/go_command_tutorial/261355
该命令链接Go的归档文件比如静态库,以及链接其所有依赖,生成一个可执行文件(含main package)。
go tool link [flags] main.a
参考:
http://cache.baiducontent.com/
该命令可以查看符号表的命令,等同于系统的nm命令,非常有用。在断点的时候,如果你不知道断点的函数符号,那么用这个命令查一下就知道了(命令处理的是二进制程序文件),第一列是地址,第二列是类型,第三列是符号。等同于nm命令。
参考:
https://studygolang.com/articles/29906
该命令可以反汇编二进制的工具,等同于系统objdump,命令解析的是二进制格式的程序文件。
go tool objdump example.o
参考:
https://studygolang.com/articles/29906
该命令把二进制文件打包成静态库。
go tool pack op file.a [name...]
参考:
http://cache.baiducontent.com/
该命令用于把测试可执行文件转化可读的json格式。
// 生成测试文件的可执行文件
参考:
https://blog.csdn.net/weixin_33772442/article/details/112098085
该命令可以生成一个Go官方文档的本地http服务,可以在线查看标准库和第三方库文档,以及项目文档,但是需要按照一定的格式去写注释。
// 安装godoc
Web页面如下:

参考:https://www.fujieace.com/golang/godoc.html
二、第三方工具
Go工具和组件汇总项目:
https://github.com/avelino/awesome-go
本地代码调试工具
参考:https://github.com/go-delve/delve
goconvey是一款针对Golang的测试框架,可以管理和运行测试用例,同时提供了丰富的断言函数,并支持很多Web界面特性。
参考:https://github.com/smartystreets/goconvey
本地排查内存泄露的工具
参考:https://github.com/uber-go/goleak
Go接口压测工具
参考:https://github.com/adjust/go-wrk
代码风格检查
参考:https://github.com/golang/lint
代码风格检查,比golint速度更快
参考:https://github.com/mgechev/revive
代码自动补全工具,可以在vim中使用
参考:https://github.com/nsf/gocode
代码重构工具
参考:https://github.com/godoctor/godoctor
查看go进程和相关信息的工具,用于诊断线上服务。
参考:https://github.com/google/gops
GoReplay是一个开源网络监控工具,可以将实时HTTP流量捕获并重放到测试环境。
参考:https://github.com/buger/goreplay
https://blog.51cto.com/axzxs/5102596
一个有用的Golang工具,Depth可帮助Web开发人员检索和可视化Go源代码依赖关系树。它可以用作独立的命令行应用程序或作为项目中的特定包。你可以通过在解析之前在Tree上设置相应的标志来添加自定义。
参考:https://github.com/KyleBanks/depth
该工具包包括各种功能和功能。Go-Swagger是Swagger 2.0的一个实现,可以序列化和反序列化swagger规范。它是RESTful API简约但强大的代表。
通过Go-Swagger,你可以swagger规范文档,验证JSON模式以及其他额外的规则。其他功能包括代码生成,基于swagger规范的API生成,基于代码的规范文档生成,扩展了的字符串格式,等等。
参考:https://github.com/go-swagger/go-swagger
交叉编译工具,可以并行编译多个平台。
参考:https://github.com/mitchellh/gox
gocyclo用来检查函数的复杂度。
# 列出了所有复杂度大于20的函数
参考:https://github.com/fzipp/gocyclo
deadcode会告诉你哪些代码片段根本没用。
find . -type d -not -path "./vendor/*" | xargs deadcode
参考:https://github.com/tsenart/deadcode
gotype会对go文件和包进行语义(semantic)和句法(syntactic)的分析,这是google提供的一个工具。
find . -name "*.go" -not -path "./vendor/*" -not -path ".git/*" -print | xargs gotype -a
参考:https://golang.org/x/tools/cmd/gotype
misspell用来拼写检查,对国内英语不太熟练的同学很有帮助。
find . -type f -not -path "./vendor/*" -print | xargs misspell
参考:https://github.com/client9/misspell
staticcheck是一个超牛的工具,提供了巨多的静态检查,就像C#生态圈的 ReSharper一样。
# 安装:
参考:
https://github.com/dominikh/go-tools/tree/master/staticcheck
goconst会查找重复的字符串,这些字符串可以抽取成常量。
goconst ./… | grep -v vendor
参考:https://github.com/jgautheron/goconst
参考资料:
1.Go命令教程:
https://www.kancloud.cn/cattong/go_command_tutorial/261351
2.Golang指南:顶级Golang框架、IDE和工具列表:
https://zhuanlan.zhihu.com/p/30432648
3.Go代码检修工具集:
go语言不需要写分号
package 包名
import ""
func main(){
//code
}
import ""
import(
""
""
)
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32
// represents a Unicode code point
float32 float64
complex64 complex128
Array types 数组
Structure types 结构体
Union types 联合
Slice types 切片,引用类型
Interface types 接口
Map types 字典,引用类型
Channel Types 通道,引用类型
Slice, Map, Channel 这三种是引用数据类型, 使用需要make
type flag byte
var 变量1, 变量2 变量类型\
例如
var x, y int
var x, y int = 2, 3
var (
x,y int
a,s = 100, "abc"
)
缺省赋值只能用在函数内部 \
x, y := 2, 3 // 会自动类型推断
常量可以只定义不使用
常量不能读地址 const 常量名 数据类型 = 值
const variable type = value
const name, size = "carrot", 100
const(
var1 type = value
var2 type = value
var3 = value // 也可以自动类型推断
)
//这种情况下, BBB和CCC的值也是120
const(
AAA = 120
BBB
CCC
)
// Golang中没有枚举类型,但是可以使用iota来更优雅的定义一个枚举
// iota从0开始递增,步长1
const(
Low = 5 * iota //Low = 0
Medium //Medium = 5
High //High = 10
)
//iota可以被中断
const(
a = iota //a = 0,默认值
b //1
c //2
d = 100 //100
e //100
f = iota //5
)
a := 1
b := byte(a)
// go中不能将非布尔类型的类型转换成布尔类型, 也不能作为布尔类型来使用
关键字展示
- if
- if else
- if (){}else if(){} else{}
- switch
- for
- goto break contunue
- 没有while!没有while!没有while! 重要的事情说三遍
if a > 1 {
//code
}
if a > 1 {//左花括号不能换行
//code
} else { //else关键字不能换行,左花括号不能换行
//code
}
if a > 1 {
//code
} else if a > 2 {
//code
} else {
//code
}
//可以不用写break,go语言执行完一个case后默认break
//default也可以不写在最后, 但是建议还是写最后
//如果不想执行完一个case后就被break可以使用fallthrough
func main() {
a, b, c, x := 1, 2, 3, 2
switch x{
case a, b:
fmt.Println("a|b")
fallthrough // 这样就不会被退出了
case c:
fmt.Println("c")
default:
fmt.Println("default")
}
}
//普通的for
for i := 0; i<max; i++ {
}
//迭代列表和切片
date := [4]string{"a","b",“c”}
for i, str := range date {
fmt.Println(i, str)
}
//迭代mqp
for k, v := range map {
fmt.Println(k,v)
}
// 死循环
for {
//code
}
// 1. 不支持内部定义函数,但是可以定义匿名函数
func main() {
func swap(x, y string) (string, string) {
return y, x
}
a, b := swap("hello", "world")
fmt.Println(a, b)
}
// 错误:syntax error: unexpected swap, expecting (
// 2. 匿名函数, 用法有点像js
func main() {
func (s string) {
fmt.Println(s)
} ("hello, go!")
}
// 3. 函数可以返回多个值, 参数也可以是可变参数
func fun_name (str string, a ...int) int {}
func fun_name (str string, a ...int) (x, y int) {}
// 4. 函数参数没有默认值即使是 下划线开头 func fun_name (str string, a int, _boo bool)
// 5. 闭包的写法, 返回一个函数的函数就是闭包闭包内的局部变量是自我维护的
func intSeq() func() int {
i := 0
return func() int {
i += 1
return i
}
}
func main(){
nextInt := intSeq()
nextInt()
nextInt()
nextInt()
}
#!/bin/bash
# 安装go
wget https://mirrors.aliyun.com/golang/go${GOLANGVERSION}.linux-amd64.tar.gz?spm=a2c6h.25603864.0.0.a6b07c45FOi9wZ -O /opt/go${GOLANGVERSION}.linux-amd64.tar.gz && cd /opt && rm -rf go${GOLANGVERSION} && \
mkdir go${GOLANGVERSION} && \
tar -zxf go${GOLANGVERSION}.linux-amd64.tar.gz -C go${GOLANGVERSION} && \
rm -f go${GOLANGVERSION}.linux-amd64.tar.gz && \
echo "export GOROOT=/opt/go${GOLANGVERSION}/go" >> /etc/profile && \
echo 'export GOPATH=/root/go' >> /etc/profile && \
echo 'PATH=$PATH:$GOROOT/bin:$GOPATH/bin' >> /etc/profile && \
source /etc/profile && \
go env -w GO111MODULE=on && \
go env -w GOPROXY=https://goproxy.cn,direct && \
# 本地k8s测试工具
go install sigs.k8s.io/kind@latest && kind --version && \
# 漏洞检测工具
go install golang.org/x/vuln/cmd/govulncheck@latest && \
# protoc编译器
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \
# grpc
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest && \
# gateway
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest && \
# openapi
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest && \
# 安装delve,用于debug go代码
go install github.com/go-delve/delve/cmd/dlv@latest && \
# 安装wire自动注入工具
go install github.com/google/wire/cmd/wire@latest
For more information, see 'go help module-auth'.
gateway2-http-command-async/cmd/appservice imports
github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/transforms imports
github.com/edgexfoundry/go-mod-core-contracts/v2/common: github.com/edgexfoundry/go-mod-core-contracts/v2@v2.1.0: verifying module: checksum mismatch
downloaded: h1:QIb6pcSYxL9F4TDnZOZk7+USnZ8Nivz1hHxmoAFNPVM=
sum.golang.org: h1:uphot3ZKOH0/aoo/Y5gr2NCRgGzy9RksWsXKtJRVEuQ=
SECURITY ERROR
This download does NOT match the one reported by the checksum server.
The bits may have been replaced on the origin server, or an attacker may
have intercepted the download attempt.
方案: 考虑换goproxy解决,有可能时代理中存储的签名和sum.golang.org存储的签名不同
在go module 项目中, 全局配置过 GOPROXY=“https://goproxy.io” 此时如果要用go get github.com/xxx/xxx 的包是完全可以导入的(实际是从 https://goproxy.io/github.com/xxx/xxx 来下载包的
但是如果要导入的包是私有仓库比如 gitpack.xxx.cn/myutil/help 实际是从 https://goproxy.io/gitpack.gitpack.xxx.cn/myutil/help 这时一定是返回http 404错误的 报错如下
If your Go version >= 1.13, the GOPRIVATE environment variable controls which modules the go command considers to be private (not available publicly) and should therefore not use the proxy or checksum database.
要导入私有git仓库的包需要设置go 环境变量GOPRIVATE go env -w GOPRIVATE=".xxx.cn" 或者直接设置环境变量GOPRIVATE=.xxx.cn 再用 go get -u gitpack.xxx.cn/myutil/help@分支名(标签名) 就可以成功导入私有包了
public void b() {
int a=3;
int b=4;
a=a^b;
b=b^a;//b=b^(a^b) ->b=a
a=a^b;//a=(a^b)^(b^a) ->a=b
System.out.println(a);//输出4
System.out.println(b);//输出3
}
package cn.anzhongwei.lean.demo.encryption;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* AES加密类
*
*/
public class AESUtils {
/**
* 密钥算法
*/
private static final String KEY_ALGORITHM = "AES";
private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
/**
* 初始化密钥
*
* @return byte[] 密钥
* @throws Exception
*/
public static byte[] initSecretKey() {
// 返回生成指定算法的秘密密钥的 KeyGenerator 对象
KeyGenerator kg = null;
try {
kg = KeyGenerator.getInstance(KEY_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return new byte[0];
}
// 初始化此密钥生成器,使其具有确定的密钥大小
// AES 要求密钥长度为 128
kg.init(128);
// 生成一个密钥
SecretKey secretKey = kg.generateKey();
return secretKey.getEncoded();
}
/**
* 转换密钥
*
* @param key
* 二进制密钥
* @return 密钥
*/
public static Key toKey(byte[] key) {
// 生成密钥
return new SecretKeySpec(key, KEY_ALGORITHM);
}
/**
* 加密
*
* @param data
* 待加密数据
* @param key
* 密钥
* @return byte[] 加密数据
* @throws Exception
*/
public static byte[] encrypt(byte[] data, Key key) throws Exception {
return encrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
}
/**
* 加密
*
* @param data
* 待加密数据
* @param key
* 二进制密钥
* @return byte[] 加密数据
* @throws Exception
*/
public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
return encrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
}
/**
* 加密
*
* @param data
* 待加密数据
* @param key
* 二进制密钥
* @param cipherAlgorithm
* 加密算法/工作模式/填充方式
* @return byte[] 加密数据
* @throws Exception
*/
public static byte[] encrypt(byte[] data, byte[] key, String cipherAlgorithm) throws Exception {
// 还原密钥
Key k = toKey(key);
return encrypt(data, k, cipherAlgorithm);
}
/**
* 加密
*
* @param data
* 待加密数据
* @param key
* 密钥
* @param cipherAlgorithm
* 加密算法/工作模式/填充方式
* @return byte[] 加密数据
* @throws Exception
*/
public static byte[] encrypt(byte[] data, Key key, String cipherAlgorithm) throws Exception {
// 实例化
Cipher cipher = Cipher.getInstance(cipherAlgorithm);
// 使用密钥初始化,设置为加密模式
cipher.init(Cipher.ENCRYPT_MODE, key);
// 执行操作
return cipher.doFinal(data);
}
/**
* 解密
*
* @param data
* 待解密数据
* @param key
* 二进制密钥
* @return byte[] 解密数据
* @throws Exception
*/
public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
return decrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
}
/**
* 解密
*
* @param data
* 待解密数据
* @param key
* 密钥
* @return byte[] 解密数据
* @throws Exception
*/
public static byte[] decrypt(byte[] data, Key key) throws Exception {
return decrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
}
/**
* 解密
*
* @param data
* 待解密数据
* @param key
* 二进制密钥
* @param cipherAlgorithm
* 加密算法/工作模式/填充方式
* @return byte[] 解密数据
* @throws Exception
*/
public static byte[] decrypt(byte[] data, byte[] key, String cipherAlgorithm) throws Exception {
// 还原密钥
Key k = toKey(key);
return decrypt(data, k, cipherAlgorithm);
}
/**
* 解密
*
* @param data
* 待解密数据
* @param key
* 密钥
* @param cipherAlgorithm
* 加密算法/工作模式/填充方式
* @return byte[] 解密数据
* @throws Exception
*/
public static byte[] decrypt(byte[] data, Key key, String cipherAlgorithm) throws Exception {
// 实例化
Cipher cipher = Cipher.getInstance(cipherAlgorithm);
// 使用密钥初始化,设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, key);
// 执行操作
return cipher.doFinal(data);
}
public static String showByteArray(byte[] data) {
if (null == data) {
return null;
}
StringBuilder sb = new StringBuilder("{");
for (byte b : data) {
sb.append(b).append(",");
}
sb.deleteCharAt(sb.length() - 1);
sb.append("}");
return sb.toString();
}
/**
* 将16进制转换为二进制
*
* @param hexStr
* @return
*/
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1)
return null;
byte[] result = new byte[hexStr.length() / 2];
for (int i = 0; i < hexStr.length() / 2; i++) {
int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
/**
* 将二进制转换成16进制
*
* @param buf
* @return
*/
public static String parseByte2HexStr(byte buf[]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
/**
* @param str
* @param key
* @return
* @throws Exception
*/
public static String aesEncrypt(String str, String key) throws Exception {
if (str == null || key == null)
return null;
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.getBytes("utf-8"), "AES"));
byte[] bytes = cipher.doFinal(str.getBytes("utf-8"));
return Base64.getEncoder().encodeToString(bytes);
}
public static String aesDecrypt(String str, String key) throws Exception {
if (str == null || key == null)
return null;
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes("utf-8"), "AES"));
byte[] bytes = Base64.getDecoder().decode(str);
bytes = cipher.doFinal(bytes);
return new String(bytes, "utf-8");
}
public static void main(String[] args) throws Exception {
// 指定key
String base64Str1 = "cmVmb3JtZXJyZWZvcm1lcg==";
byte[] bs = Base64.getDecoder().decode(base64Str1);
System.out.println("base64Str1解密:"+ Arrays.toString(bs));
System.out.println("base64Str1解密:" + showByteArray(Base64.getDecoder().decode(base64Str1)));
Key key1 = toKey(Base64.getDecoder().decode(base64Str1));
String data1 = "{\"aaa\":\"bbb\"}";
System.out.println("明文:" + data1);
byte[] encryptData1 = encrypt(data1.getBytes(), key1);
String encryptStr1=parseByte2HexStr(encryptData1);
System.out.println("加密后数据1: byte[]:" + showByteArray(encryptData1));
System.out.println("加密后数据1: Byte2HexStr:" + encryptStr1);
System.out.println();
byte[] encryptStrByte1 = parseHexStr2Byte(encryptStr1);
byte[] decryptData1 = decrypt(encryptStrByte1, key1);
System.out.println("解密后数据: byte[]:" + showByteArray(decryptData1));
System.out.println("解密后数据: string:" + new String(decryptData1));
System.out.println("--------------------------------------------------------");
byte[] initKey = initSecretKey();
System.out.println("key:" + Arrays.toString(Base64.getEncoder().encode(initKey)));
System.out.println("key:" + showByteArray(initKey));
Key key2 = toKey(initKey);
String data2 = "{\"ccc\":\"ddd\"}";
System.out.println("明文:" + data2);
byte[] encryptData2 = encrypt(data2.getBytes(), key2);
String encryptStr2=parseByte2HexStr(encryptData2);
System.out.println("加密后数据1: byte[]:" + showByteArray(encryptData2));
System.out.println("加密后数据1: Byte2HexStr:" + encryptStr2);
System.out.println();
byte[] encryptStrByte2 = parseHexStr2Byte(encryptStr2);
byte[] decryptData2 = decrypt(encryptStrByte2, key2);
System.out.println("解密后数据: byte[]:" + showByteArray(decryptData2));
System.out.println("解密后数据: string:" + new String(decryptData2));
}
}
package cn.anzhongwei.lean.demo.alibabafastjsondemo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import java.util.Map;
public class JsonToMapDemo1 {
public static void main(String[] args){
String str = "{\"0\":\"zhangsan\",\"1\":\"lisi\",\"2\":\"wangwu\",\"3\":\"maliu\"}";
//第一种方式
Map maps = (Map) JSON.parse(str);
System.out.println("这个是用JSON类来解析JSON字符串!!!");
for (Object map : maps.entrySet()){
System.out.println(((Map.Entry)map).getKey()+" " + ((Map.Entry)map).getValue());
}
//第二种方式
Map mapTypes = JSON.parseObject(str);
System.out.println("这个是用JSON类的parseObject来解析JSON字符串!!!");
for (Object obj : mapTypes.keySet()){
System.out.println("key为:"+obj+"值为:"+mapTypes.get(obj));
}
//第三种方式
Map mapType = JSON.parseObject(str,Map.class);
System.out.println("这个是用JSON类,指定解析类型,来解析JSON字符串!!!");
for (Object obj : mapType.keySet()){
System.out.println("key为:"+obj+"值为:"+mapType.get(obj));
}
//第四种方式
Map json = (Map) JSONObject.parse(str);
System.out.println("这个是用JSONObject类的parse方法来解析JSON字符串!!!");
for (Object map : json.entrySet()){
System.out.println(((Map.Entry)map).getKey()+" "+((Map.Entry)map).getValue());
}
//第五种方式
JSONObject jsonObject = JSONObject.parseObject(str);
System.out.println("这个是用JSONObject的parseObject方法来解析JSON字符串!!!");
for (Object map : json.entrySet()){
System.out.println(((Map.Entry)map).getKey()+" "+((Map.Entry)map).getValue());
}
//第六种方式
Map mapObj = JSONObject.parseObject(str,Map.class);
System.out.println("这个是用JSONObject的parseObject方法并执行返回类型来解析JSON字符串!!!");
for (Object map: json.entrySet()){
System.out.println(((Map.Entry)map).getKey()+" "+((Map.Entry)map).getValue());
}
String strArr = "{{\"0\":\"zhangsan\",\"1\":\"lisi\",\"2\":\"wangwu\",\"3\":\"maliu\"}," +
"{\"00\":\"zhangsan\",\"11\":\"lisi\",\"22\":\"wangwu\",\"33\":\"maliu\"}}";
// JSONArray.parse()
System.out.println(json);
}
}
最终输出结果为:
这个是用JSON类来解析JSON字符串!!!
0 zhangsan
1 lisi
2 wangwu
3 maliu
这个是用JSON类的parseObject来解析JSON字符串!!!
key为:0值为:zhangsan
key为:1值为:lisi
key为:2值为:wangwu
key为:3值为:maliu
这个是用JSON类,指定解析类型,来解析JSON字符串!!!
key为:0值为:zhangsan
key为:1值为:lisi
key为:2值为:wangwu
key为:3值为:maliu
这个是用JSONObject类的parse方法来解析JSON字符串!!!
0 zhangsan
1 lisi
2 wangwu
3 maliu
这个是用JSONObject的parseObject方法来解析JSON字符串!!!
0 zhangsan
1 lisi
2 wangwu
3 maliu
这个是用JSONObject的parseObject方法并执行返回类型来解析JSON字符串!!!
0 zhangsan
1 lisi
2 wangwu
3 maliu
{"0":"zhangsan","1":"lisi","2":"wangwu","3":"maliu"}
package cn.anzhongwei.lean.demo.intandinteger;
// java 9 以后 此初始化已经标记为废弃,推荐使用 Integer.valueOf("100");
public class IntegerTest {
public static void main(String[] args) {
int a = 100;
Integer b = 100;
if ( a == b ) {
//原因是由于此区间的只都是由IntegerCache.cache而来,具体查看Integer.valueOf(n)方法
//其中IntegerCache.cache static final Integer cache[];
System.out.println("-128 - 127 之间的int类型是在常量池中, ");
}
// 但是 Integer.valueOf("100");
Integer c = Integer.valueOf("300");
Integer d = Integer.valueOf("300");
if ( c != d ) {
System.out.println("Integer.valueOf(\"100\") 初始化的127以上的数字是放到推空间的");
}
Integer e = Integer.valueOf("100");
Integer f = Integer.valueOf("100");
if ( e == f ) {
System.out.println("Integer.valueOf(\"100\") 初始化的127以内的还是从常量池中获取");
}
Integer g = Integer.valueOf("400");
int h = 400;
if ( g == h ) {
System.out.println("Integer会向下转型");
}
}
}
class User
package cn.anzhongwei.lean.demo.java传参;
public class User {
private String name;
private String Gender;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return Gender;
}
public void setGender(String gender) {
Gender = gender;
}
}
package cn.anzhongwei.lean.demo.java传参;
/**
* 要理解java的传参
* 结果:
主类中实例化user1-hash:cn.anzhongwei.demo.java传参.User@4d7e1886
主类中实例化参数:Name=Hollis
方法中接受到参数user-hash:cn.anzhongwei.demo.java传参.User@4d7e1886 // 与main函数同一个对象
方法中重新实例化参数user-hash:cn.anzhongwei.demo.java传参.User@3cd1a2f1 // 产生新对象,但是只在局部有效
方法中重新实例化参数:Name=hollischuang
经过方法后的user1-hashcn.anzhongwei.demo.java传参.User@4d7e1886 //user1并没有因为在函数中局部参数实例化而发生变化
经过方法后的参数xiaoming //实例属性会被函数中的更改而发生变化
*/
public class PassOn {
public void pass(User user) {
System.out.println("方法中接受到参数user-hash:"+user);
user.setName("xiaoming");
user = new User();
user.setName("hollischuang");
System.out.println("方法中重新实例化参数user-hash:"+user);
System.out.println("方法中重新实例化参数:Name="+user.getName());
}
public static void main(String[] args) {
PassOn pt = new PassOn();
User user1 = new User();
System.out.println("主类中实例化user1-hash:"+user1);
user1.setName("Hollis");
user1.setGender("Male");
System.out.println("主类中实例化参数:Name="+user1.getName());
pt.pass(user1);
System.out.println("经过方法后的user1-hash" + user1);
System.out.println("经过方法后的参数" + user1.getName());
}
}
package cn.anzhongwei.lean.demo.optional;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Optional;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
private String name;
private Integer age;
private Address address;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public Optional<String> optionalGetName() {
return Optional.ofNullable(this.name);
}
}
package cn.anzhongwei.lean.demo.optional;
import lombok.Data;
@Data
public class Address {
private String city;
private String addInfo;
}
package cn.anzhongwei.lean.demo.optional;
import org.junit.Test;
import java.util.*;
import java.util.stream.Collectors;
public class OptionalTest {
/**
* Optional(T value),empty(),of(T value),ofNullable(T value)
* Optional(T value) 是private的属于内部方法
*/
@Test
public void test1() {
//of(T value) 是实际上调用了Optional(T value) 而其内部的requireNonNull方法源码中判断value==null时会抛出空指针异常
//在开发中如果不想隐藏空指针异常时 使用of函数, 不过这种情况几乎不用
//Optional<Person> p1 = Optional.of(null);
//所以如果要创建一个value为null的Optional对象则应该使用 empty()
Optional<Person> pnull = Optional.empty();
//而ofNullable(T value) 则是一个三元表达式 return value == null ? empty() : of(value);
Optional<Person> p0 = Optional.ofNullable(null);
Optional<Person> p1 = Optional.ofNullable(new Person("zhangs", 20));
}
/**
* orElse(T other),orElseGet(Supplier other)和orElseThrow(Supplier exceptionSupplier)
* 这三个函数放一组进行记忆,都是在构造函数传入的value值为null时,进行调用的。
* orElse 和 orElseGet 没区别 可以认为时完全一样的代码
*
* orElseThrow 则会当value为空时抛出一个自定义异常
*/
@Test
public void test2() {
Person p = null;
//当p值不为null时,orElse函数依然会执行createUser()方法
p = Optional.ofNullable(p).orElse(new Person("zhangsan", 20));
System.out.println("调用orElse 当p为null时:"+p);
//而当p不为空,则相当于使用get()方法
p = Optional.ofNullable(p).orElse(new Person("王五", 60));
System.out.println("调用orElse 此时p不为null时:"+p);
//
p = null;
//重新赋值p=null
System.out.println("重新赋值p=:"+p);
p = Optional.ofNullable(p).orElseGet(() -> new Person("lisi", 30));
System.out.println("orElseGet 当p为null时:"+p);
p = Optional.ofNullable(p).orElseGet(() -> new Person("zhaoliu", 34));
System.out.println("orElseGet 此时p不为null时:"+p);
}
@Test
public void testOrElseThrow() {
Person p = new Person("lisi", 30);
p = Optional.ofNullable(p).orElseThrow(()-> new RuntimeException("用户不存在"));
System.out.println("orElseThrow 此时p不为null时:"+p);
p = null;
p = Optional.ofNullable(p).orElseThrow(()-> new RuntimeException("用户不存在"));
}
/**
* map(Function mapper)和flatMap(Function> mapper)
* 这两个函数做的是转换值的操作
*/
@Test
public void test3() {
Person p = new Person("lisi", 30);
String name = Optional.ofNullable(p).map( person -> person.getName()).get();
System.out.println("name="+name);
String optionalName = Optional.ofNullable(p).flatMap(person -> person.optionalGetName()).get();
System.out.println("optionalName="+optionalName);
}
/**
* isPresent()和ifPresent(Consumer consumer)
* isPresent即判断value值是否为空,而ifPresent就是在value值不为空时,做一些操作。
*/
@Test
public void test4() {
Person p = null;
Optional.ofNullable(p).ifPresent(person -> {
});
}
/**
* filter(Predicate predicate)
*/
@Test
public void test5() {
Person p = null;
//如果 p.name 长度小于6则返回, 否则返回EMPTY
Optional<Person> p1 = Optional.ofNullable(p).filter(u -> u.getName().length()<6);
}
/**
* 实战写法1
*/
public String getCity(Person person) throws Exception{
if(person!=null){
if(person.getAddress()!=null){
Address address = person.getAddress();
if(address.getCity()!=null){
return address.getCity();
}
}
}
throw new RuntimeException("取值错误");
}
public String getCity2(Person person) throws Exception{
return Optional.ofNullable(person)
.map(u-> u.getAddress())
.map(a->a.getCity())
.orElseThrow(()->new Exception("取值错误"));
}
/**
* 实战写法2
*/
public void dosomething(Person person) {
if (Objects.nonNull(person)) {
//......
}
Optional.ofNullable(person).ifPresent(person1 -> {
//......
});
}
/**
* 实战写法3
*/
public Person getUser1(Person user){
if(user!=null){
String name = user.getName();
if(!"zhangsan".equals(name)){
user = new Person();
user.setName("zhangsan");
}
}else{
user = new Person();
user.setName("zhangsan");
}
return user;
}
public Person getUser2(Person user) {
return Optional.ofNullable(user)
.filter(u->"zhangsan".equals(u.getName()))
.orElseGet(()-> {
Person user1 = new Person();
user1.setName("zhangsan");
return user1;
});
}
public Person getUser3(Person user){
if(user!=null){
String name = user.getName();
if(!"zhangsan".equals(name)){
user.setName("zhangsan");
}
}else{
user = new Person();
user.setName("zhangsan");
}
return user;
}
public Person getUser4(Person user) {
//这个逻辑一般来说不会这么写, 大多数情况下都是不为空且不为zhangsan时, 将name赋值成zhangsan,而不是重新实例化
return Optional.ofNullable(user)
.filter(u->"zhangsan".equals(u.getName()))
.orElseGet(()-> {
if (Objects.nonNull(user)) {
user.setName("zhangsan");
return user;
} else {
Person user1 = new Person();
user1.setName("zhangsan");
return user1;
}
});
}
@Test
public void test6() {
//getUser1 和 getUser2 只要name不等于 zhangsan, 就重新初始化
Person p = new Person("wangwu", 26);
Person p1 = getUser1(p);
System.out.println("p1:"+p1);
p = new Person("lisi", 23);
Person p2 = getUser2(p);
System.out.println("p2:"+p2);
//getUser3 和 getUser4 name不等于 zhangsan, 只改变name=张三, 其他内容不变
p = new Person("zhaosi", 26);
Person p3 = getUser3(p);
System.out.println("p3:"+p3);
p = new Person("liuqi", 26);
Person p4 = getUser4(p);
System.out.println("p4:"+p4);
}
@Test
public void test7() {
List<Person> personList = new ArrayList<>();
Person p1 = new Person("zhangsan", 22);personList.add(p1);
Person p2 = new Person("zhaosi", 29);personList.add(p2);
Person p3 = new Person("wangwu", 22);personList.add(p3);
Person p4 = new Person("lisen", 35);personList.add(p4);
Person p5 = new Person("liuming", 248);personList.add(p5);
Optional<Person> personOptional = personList.stream().filter(p -> p.getAge() > 22).findFirst();
Person pp1 = personOptional.orElseGet(() -> personList.get(0));
System.out.println(pp1);
Person pp2 = personOptional.orElse(null);
System.out.println(pp2);
List<Person> personList2 = Optional.ofNullable(personList).orElseGet(ArrayList::new);
System.out.println(personList2);
Map<String, List<Person>> pMap = new HashMap<>();
// List<Person> personList3 = Optional.ofNullable(pMap.get("随便写"))
// //这种写法主要为了后面stream()的流式处理方式不会有空指针
// //这样会空指针
// .get()
// .stream().collect(Collectors.toList());
// System.out.println(personList3);
List<Person> personList4 = Optional.ofNullable(pMap.get("随便写"))
//这种写法主要为了后面stream()的流式处理方式不会有空指针
.orElseGet(ArrayList::new)
.stream().collect(Collectors.toList());
System.out.println(personList4);
}
}
package cn.anzhongwei.lean.demo.pdftoimg;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class PdfUtil {
public static void main(String[] args) {
String source = "C:\\Home\\22.pdf";
String desFileName = "invoice";
String desFilePath = "C:\\Home\\ddd";
String imageType = "png";
Long t1 = System.currentTimeMillis();
List<String> pair = PdfUtil.pdfToImg(source, desFilePath, desFileName, imageType);
Long t2 = System.currentTimeMillis();
System.out.println(t2-t1);
pair.forEach(System.out::println);
}
public static List<String> pdfToImg(String source, String desFilePath, String desFileName, String imageType) {
//通过给定的源路径名字符串创建一个File实例
File file = new File(source);
if (!file.exists()) {
throw new RuntimeException("文件不存在,无法转化");
}
//目录不存在则创建目录
File destination = new File(desFilePath);
if (!destination.exists()) {
boolean flag = destination.mkdirs();
System.out.println("创建文件夹结果:" + flag);
}
PDDocument doc = null;
try {
//加载PDF文件
doc = PDDocument.load(file);
PDFRenderer renderer = new PDFRenderer(doc);
//获取PDF文档的页数
int pageCount = doc.getNumberOfPages();
System.out.println("文档一共" + pageCount + "页");
List<String> fileList = new ArrayList<>();
for (int i = 0; i < pageCount; i++) {
//只有一页的时候文件名为传入的文件名,大于一页的文件名为:文件名_自增加数字(从1开始)
String realFileName = pageCount > 1 ? desFileName + "_" + (i + 1) : desFileName;
//每一页通过分辨率和颜色值进行转化
BufferedImage bufferedImage = renderer.renderImageWithDPI(i, 96*2, ImageType.RGB);
String filePath = desFilePath + File.separator + realFileName + "." + imageType;
//写入文件
ImageIO.write(bufferedImage, imageType, new File(filePath));
//文件名存入list
fileList.add(filePath);
}
return fileList;
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("PDF转化图片异常,无法转化");
} finally {
try {
if (doc != null) {
doc.close();
}
} catch (IOException e) {
System.out.println("关闭文档失败");
e.printStackTrace();
}
}
}
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
@Data
@ToString
@AllArgsConstructor
public class Person implements Serializable {
private String name;
private int age;
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
@Data
@ToString
@AllArgsConstructor
public class Teacher implements Serializable {
private String name;
private Person person;
}
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
// TODO 还没测试成功
public class WriteObject {
public static void main(String[] args) {
// try (//创建一个ObjectOutputStream输出流
// ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Home\\teacher.txt"))) {
// //将对象序列化到文件s
// Person person = new Person("9龙", 23);
// Teacher teacher = new Teacher("路飞", person);
// oos.writeObject(teacher);
// } catch (Exception e) {
// e.printStackTrace();
// }
try (//创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\teacher2.txt"))) {
//将对象序列化到文件s
Person person = new Person("路飞", 20);
Teacher t1 = new Teacher("雷利", person);
Teacher t2 = new Teacher("红发香克斯", person);
//依次将4个对象写入输入流
oos.writeObject(t1);
oos.writeObject(t2);
System.out.println(t2.hashCode());
oos.writeObject(person);
oos.writeObject(t2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class ReadObject {
public static void main(String[] args) {
// try (//创建一个ObjectInputStream输入流
// ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Home\\teacher.txt"))) {
// Teacher teacher = (Teacher) ois.readObject();
// System.out.println(teacher);
// } catch (Exception e) {
// e.printStackTrace();
// }
try (//创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\teacher2.txt"))) {
Teacher t1 = (Teacher) ois.readObject();
Teacher t2 = (Teacher) ois.readObject();
Person p = (Person) ois.readObject();
Teacher t3 = (Teacher) ois.readObject();
System.out.println(t1 == t2);
System.out.println(t1.getPerson() == p);
System.out.println(t2.getPerson() == p);
System.out.println(t2 == t3);
System.out.println(t2.hashCode());
System.out.println(t3.hashCode());
System.out.println(t1.getPerson() == t2.getPerson());
} catch (Exception e) {
e.printStackTrace();
}
}
}
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Base64;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
<!--apacheftp-->
<!--导入commons-net依赖-->
<!--https://mvnrepository.com/artifact/commons-net/commons-net-->
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.6</version>
</dependency>
<!-- 引入Apache的commons-lang3包,方便操作字符串-->
<!--https://mvnrepository.com/artifact/org.apache.commons/commons-lang3-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8</version>
</dependency>
<!--引入Apachecommons-io包,方便操作文件-->
<!--https://mvnrepository.com/artifact/commons-io/commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
*/
public class SimpleFTPClient {
private static final Logger logger = LoggerFactory.getLogger(SimpleFTPClient.class);
private static FTPClient connectFtpServer(String url,String userName,String password){
String str = url.replaceFirst("ftp://", "").replaceFirst("FTP://", "");
str=str.replaceAll("/+", "/");
int endIndex = str.indexOf("/");
str = str.substring(0,endIndex);
url = str;//"ftp://"+str+"/";
String[] addr = url.split(":");
FTPClient ftpClient = new FTPClient();
ftpClient.setConnectTimeout(1000*5);//设置连接超时时间
// ftpClient.setControlEncoding("GBK");
ftpClient.setControlEncoding("utf-8");//设置ftp字符集
ftpClient.enterLocalPassiveMode();//设置被动模式,文件传输端口设置
try {
ftpClient.connect(addr[0],Integer.parseInt(addr[1]));
if(null==userName||"".equals(userName)){
userName = "anonymous";
password = "password";
}
boolean isLoginSuccess = ftpClient.login(userName,password);
logger.info("isLoginSuccess="+isLoginSuccess);
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);//设置文件传输模式为二进制,可以保证传输的内容不会被改变
int replyCode = ftpClient.getReplyCode();
if (!FTPReply.isPositiveCompletion(replyCode)){
logger.error("connect ftp {} failed",url);
ftpClient.abort();
ftpClient.disconnect();
return null;
}
logger.info("replyCode==========={}",replyCode);
} catch (IOException e) {
logger.error("connect fail ------->>>{}",e);
return null;
}
return ftpClient;
}
/**
*
* @param inputStream 待上传文件的输入流
* @param originName 文件保存时的名字
*/
public void uploadFile(String url,String userName,String password,InputStream inputStream, String originName){
FTPClient ftpClient = connectFtpServer(url,userName,password);
if (ftpClient == null){
return;
}
/* try {
ftpClient.changeWorkingDirectory(remoteDir);//进入到文件保存的目录
Boolean isSuccess = ftpClient.storeFile(originName,inputStream);//保存文件
if (!isSuccess){
throw new BusinessException(ResponseCode.UPLOAD_FILE_FAIL_CODE,originName+"---》上传失败!");
}
logger.info("{}---》上传成功!",originName);
ftpClient.logout();
} catch (IOException e) {
logger.error("{}---》上传失败!",originName);
throw new BusinessException(ResponseCode.UPLOAD_FILE_FAIL_CODE,originName+"上传失败!");
}finally {
if (ftpClient.isConnected()){
try {
ftpClient.disconnect();
} catch (IOException e) {
logger.error("disconnect fail ------->>>{}",e.getCause());
}
}
} */
}
/**
* 读ftp上的文件,并将其转换成base64
* @param remoteFileName ftp服务器上的文件名
* @return
*/
public static String readFileToBase64(String url,String userName,String password){
logger.info("url={}",url);
FTPClient ftpClient = connectFtpServer(url,userName,password);
if (ftpClient == null){
return null;
}
logger.info("ftpClient={}",ftpClient.isAvailable()&&ftpClient.isConnected());
String base64 = "";
InputStream inputStream = null;
ByteArrayOutputStream os=null;
String remoteDir;
String str = url.replaceFirst("ftp://", "").replaceFirst("FTP://", "");
str=str.replaceAll("/+", "/");
int beginIndex = str.indexOf("/");
int endIndex = str.lastIndexOf("/");
remoteDir=str.substring(beginIndex,endIndex+1);
logger.info("remoteDir={}",remoteDir);
String remoteFileName = str.substring(endIndex+1);
logger.info("remoteFileName={}",remoteFileName);
try {
boolean chDirRes = ftpClient.changeWorkingDirectory(remoteDir);
logger.info("changeWorkingDirectory={}",chDirRes);
ftpClient.enterLocalPassiveMode();
FTPFile[] ftpFiles = ftpClient.listFiles(remoteDir);
logger.info("ftpFiles.length={}",ftpFiles.length);
Boolean flag = false;
//遍历当前目录下的文件,判断要读取的文件是否在当前目录下
for (FTPFile ftpFile:ftpFiles){
if (ftpFile.getName().equals(remoteFileName)){
flag = true;
break;
}
}
if (!flag){
logger.error("directory:{}下没有 {}",remoteDir,remoteFileName);
return null;
}
/* // 下载ftp上指定文件到本地
File localFile = new File("D:" + File.separator +"abc_"+ remoteFileName);
boolean downloaded = ftpClient.retrieveFile(remoteFileName,
new FileOutputStream(localFile));
System.out.println(ftpClient.getReplyString()); */
logger.info("开始获取文件流");
//获取待读文件输入流
inputStream = ftpClient.retrieveFileStream(remoteFileName); // remoteDir+remoteFileName
// //inputStream.available() 获取返回在不阻塞的情况下能读取的字节数,正常情况是文件的大小
logger.info("完成获取文件流");
byte[] bytes = new byte[1024];
os = new ByteArrayOutputStream();
int len=-1;
while( (len=inputStream.read(bytes))!=-1){//将文件数据读到字节数组中
os.write(bytes, 0, len);
}
os.flush();
byte[] fileBytes = os.toByteArray();
/* File file = new File("d:/"+remoteFileName);
OutputStream fos = new FileOutputStream(file);
try {
fos.write(fileBytes);
fos.flush();
} catch (Exception e) {
//TODO: handle exception
}
finally{
fos.close();
} */
int fileSize = fileBytes.length;
logger.info("fileSize={}",fileSize);
Base64.Encoder encoder = Base64.getEncoder();
base64 = encoder.encodeToString(fileBytes);
logger.debug("base64 img="+base64);
logger.info("read file {} success",remoteFileName);
if(!ftpClient.completePendingCommand()) {
ftpClient.logout();
ftpClient.disconnect();
logger.error("File transfer failed.");
return null;
}
ftpClient.logout();
} catch (IOException e) {
logger.error("read file fail ----->>>{}",e.getCause());
return null;
}finally {
if (ftpClient.isConnected()){
try {
ftpClient.disconnect();
} catch (IOException e) {
logger.error("disconnect fail ------->>>{}",e.getCause());
}
}
if (inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
logger.error("inputStream close fail -------- {}",e.getCause());
}
}
if (os != null){
try {
os.close();
} catch (IOException e) {
logger.error("os close fail -------- {}",e.getCause());
}
}
}
return base64;
}
/**
* 文件下载
* @param remoteFileName ftp上的文件名
* @param localFileName 本地文件名
*/
public void download(String url,String userName,String password,String remoteFileName,String localFileName){
FTPClient ftpClient = connectFtpServer(url,userName,password);
if (ftpClient == null){
return ;
}
OutputStream outputStream = null;
/* try {
ftpClient.changeWorkingDirectory(remoteDir);
FTPFile[] ftpFiles = ftpClient.listFiles(remoteDir);
Boolean flag = false;
//遍历当前目录下的文件,判断是否存在待下载的文件
for (FTPFile ftpFile:ftpFiles){
if (ftpFile.getName().equals(remoteFileName)){
flag = true;
break;
}
}
if (!flag){
logger.error("directory:{}下没有 {}",remoteDir,remoteFileName);
return ;
}
outputStream = new FileOutputStream(localDir+localFileName);//创建文件输出流
Boolean isSuccess = ftpClient.retrieveFile(remoteFileName,outputStream); //下载文件
if (!isSuccess){
logger.error("download file 【{}】 fail",remoteFileName);
}
logger.info("download file success");
ftpClient.logout();
} catch (IOException e) {
logger.error("download file 【{}】 fail ------->>>{}",remoteFileName,e.getCause());
}finally {
if (ftpClient.isConnected()){
try {
ftpClient.disconnect();
} catch (IOException e) {
logger.error("disconnect fail ------->>>{}",e.getCause());
}
}
if (outputStream != null){
try {
outputStream.close();
} catch (IOException e) {
logger.error("outputStream close fail ------->>>{}",e.getCause());
}
}
} */
}
public static void main(String[] args) {
// String ftpPath = "ftp://192.168.1.3:8010/recordsImg/2019-05-31/STRANGERBABY_1559265983072.jpg";
// readFileToBase64(ftpPath,"","");
String url="ftp://106.12.195.197:21/";
FTPClient ftpClient = connectFtpServer(url,"myftp","123456");
if (ftpClient == null){
return ;
}
logger.info("ftpClient={}",ftpClient.isAvailable()&&ftpClient.isConnected());
String base64 = "";
InputStream inputStream = null;
ByteArrayOutputStream os=null;
String remoteDir;
String str = url.replaceFirst("ftp://", "").replaceFirst("FTP://", "");
str=str.replaceAll("/+", "/");
int beginIndex = str.indexOf("/");
// int endIndex = str.lastIndexOf("/");
// remoteDir=str.substring(beginIndex,endIndex+1);
String remotePath =str.substring(beginIndex);
logger.info("remotePath={}",remotePath);
String status="nil";
try {
status=ftpClient.getStatus(remotePath);
} catch (Exception e) {
//TODO: handle exception
e.printStackTrace();
}
logger.info("ftpClient.getStatus={}",status);
try {
// boolean chDirRes = ftpClient.changeWorkingDirectory(remoteDir);
// logger.info("changeWorkingDirectory={}",chDirRes);
// ftpClient.enterLocalPassiveMode();
/* FTPFile[] ftpFiles = ftpClient.listFiles(remoteDir);
logger.info("ftpFiles.length={}",ftpFiles.length);
Boolean flag = false;
//遍历当前目录下的文件,判断要读取的文件是否在当前目录下
for (FTPFile ftpFile:ftpFiles){
logger.info("ftpFiles------{}",ftpFile.getName());
} */
// boolean renameRes = ftpClient.rename("/imgfacebak/bb1.png", "/test/a.png");
// logger.info("renameRes={}",renameRes);
// if(!ftpClient.completePendingCommand()) {
// ftpClient.logout();
// ftpClient.disconnect();
// logger.error("File transfer failed.");
// return ;
// }
ftpClient.logout();
} catch (IOException e) {
logger.error("read file fail ----->>>{}",e.getCause());
return ;
}finally {
if (ftpClient.isConnected()){
try {
ftpClient.disconnect();
} catch (IOException e) {
logger.error("disconnect fail ------->>>{}",e.getCause());
}
}
}
}
}
添加配置项
cloud:
inetutils:
preferred-networks: 192.168.*
spring:
servlet:
multipart:
#配置文件传输
enabled: true
file-size-threshold: 0B
#单个文件的最大上限
max-file-size: 100MB
#单个请求的文件总大小上限
max-request-size: 1000MB
实现方式:Mockito + JUnit5(无Spring注解)
特点:
仅测试类自身逻辑
执行速度最快(毫秒级)
适用场景:Service工具类、纯POJO逻辑测试
代码示例:
@ExtendWith(MockitoExtension.class)
class GoodsControllerTest {
private MockMvc mockMvc;
// 需要模拟的控制器
@InjectMocks // 可以注入要模拟的类
private GoodsController goodsController;
// 需要打桩的Service
@Mock
GoodsService goodsService;
@BeforeEach
public void initOne() {
// 通过goodsController构建mvc模拟对象
mockMvc = MockMvcBuilders.standaloneSetup(goodsController).build();
}
}
实现方式:@WebMvcTest/@DataJpaTest等
特点:
仅加载相关模块组件(如MVC层/JPA层)
启动较快(秒级)
适用场景:控制器测试、Repository接口测试
代码示例:
@WebMvcTest(HomeController.class) // HomeController要测试那个控制器
class GoodsControllerTest {
@Autowired
private MockMvc mockMvc;
// 需要打桩的Service
@MockitoBean
GoodsService goodsService;
}
实现方式:@SpringBootTest
特点:
加载完整Spring容器
支持真实数据库交互
启动慢(10秒+)
适用场景:端到端测试、多组件协同测试
代码示例:
@AutoConfigureMockMvc
@SpringBootTest
class GoodsControllerTest {
@Autowired
private MockMvc mockMvc;
// 需要模拟的控制器
@InjectMocks // 可以注入要模拟的类
private GoodsController goodsController;
// 需要打桩的Service
@MockitoBean
GoodsService goodsService;
}
测试用例项目源代码: https://gitee.com/azhw/rookiedev-example/tree/master/编程/编程语言/Java/最佳实践/unittest/SpringBoot3UnitTest
Service测试: no01servicetest
Controller测试: no02ControllerTest
Controller对Service打桩&Service对Mapper打桩的测试: no03MockitoService
对文件上传下载的测试: no04FileUploadDownload
使用H2数据库对数据写入进行实际测试: no05H2DBTest
在 no03MockitoService 其实已经包含了该测试内容
测试Controller 对 Cookie 、 Session 、 Header 的读写: no06CookieSessionHeaderTest
使用嵌入式redis服务器(embedded-redis)对Redis进行测试: no07RedisTest
这种场景只有使用完整的spring容器方式进行单元测试能进行测试, 其他两种(完全脱离spring和部分启动的方式只能对redisTemplate进行打桩测试)
测试SrpingBoot的Filter: no08SpringFilterTest
演示controller使用thymeleaf模板而不是返回json时的单元测试: no09ThymeleafTest
异步调用测试: no10AsyncTest
Service超时测试: no11TimeoutTest
普通的类中(不是spring bean) 静态方法和非静态方法的打桩测试:no12NoSpringBeanMethodMockTest
import lombok.Data;
import org.junit.Test;
import java.util.*;
import java.util.stream.Collectors;
public class StreamAPI {
/**
*
* Comparator.reverseOrder()是让某个条件进行倒序排序.
* reversed()是让他前面的字段进行倒序。
*
* 例如:Comparator.comparing(名称).reversed(),此时名称倒序;
* Comparator.comparing(名称).thenComparing(状态).reversed(),此时名称和状态进行倒序。
*
* Comparator.comparing(名称).thenComparing(状态).reversed().thenComparing(年龄),此时名称和状态会倒序,年龄会升序。
*
* 加多个.reversed()
* Comparator.comparing(状态).reversed().thenComparing(年龄).reversed(),最终结果是只有年龄倒排,状态未排序
*/
/**
* 练习1:让年龄进行倒序排序。
*/
@Test
public void test1() {
/*
* Comparator.comparing(放实体类名称::放列名):添加排序字段;
* reversed():倒序。
* collect(Collectors.toList()):转成集合。
* forEach(System.out::println):打印循环。
*/
List<User> userList = generatorUserList();
// userList.stream()
// .sorted(Comparator.comparing(User::getAge).reversed())//reversed()是让其前面得所有字段都倒排
// .collect(Collectors.toList())
// .forEach(System.out::println);
// List<User> userList2 = generatorUserList();
// userList2.stream()
// .sorted(Comparator.comparing(User::getAge, Comparator.reverseOrder()))//Comparator.reverseOrder()是让某个条件进行倒序排序
// .collect(Collectors.toList()).forEach(System.out::println);
userList.stream().sorted(Comparator.comparing(User::getAge).reversed())
.collect(Collectors.toList()).forEach(System.out::println);
}
/**
* 练习2:让使用状态进行升序排序。
*/
@Test
public void test2() {
List<User> userList = generatorUserList();
//默认是升序,所以只需要把排序字段放进去就行了。
userList.stream().sorted(Comparator.comparing(User::getAge)).collect(Collectors.toList()).forEach(System.out::println);
}
/**
* 练习3:让使用先状态和后年龄都进行倒序排序。
*/
@Test
public void test3() {
List<User> userList = generatorUserList();
userList.stream()
//先根据使用状态倒序,而王五使用状态为2,所以王五排在第一。
//而李四和张三使用状态都是1,就会触发另一个排序条件根据年龄排序
.sorted(Comparator.comparing(User::getState).thenComparing(User::getAge).reversed())
.collect(Collectors.toList())
.forEach(System.out::println);
//写法2
List<User> userList2 = generatorUserList();
userList2.stream()
.sorted(
Comparator.comparing(User::getState, Comparator.reverseOrder())//根据state倒排
.thenComparing(User::getAge, Comparator.reverseOrder())//根据age倒排
)
.forEach(System.out::println);
}
/**
* 练习4:让使用状态和年龄都进行升序排序。
*/
@Test
public void test4() {
List<User> userList = generatorUserList();
//默认就是升序
userList.stream().sorted(Comparator.comparing(User::getState).thenComparing(User::getAge)).forEach(System.out::println);
}
/**
* 练习5:让使用状态进行升序排序,年龄进行倒序排序,创建时间进行倒序排序。名字正排序
* 注意:假如多个排序条件的排序方向不一致,需要倒序的字段应该用Comparator.reverseOrder(),每个倒排得字段单独写。
*/
@Test
public void test5() {
List<User> userList = generatorUserList();
userList.stream()
.sorted(
Comparator.comparing(User::getState)
.thenComparing(User::getAge, Comparator.reverseOrder())
.thenComparing(User::getCreateTime,Comparator.reverseOrder())
.thenComparing(User::getName)
)
.forEach(System.out::println);
}
/**
* 练习6,按 State 分组 组成 Map<Ingeter, List<User>>的结构
*/
@Test
public void test6() {
List<User> userList = generatorUserList();
Map<Integer, List<User>> stateToUserList = userList.stream().collect(Collectors.groupingBy(User::getState));
System.out.println(stateToUserList);
}
/**
* 联系7 根据姓名作为key, User对象为val,组成map<String, User>
* 如果是根据state, 因为元素不唯一, 所以会抛出异常
* 根据姓名为key, age为val组成 map<String, Ingeter>
*/
@Test
public void test7() {
List<User> userList = generatorUserList();
Map<String, User> nameToUser = userList.stream().collect(Collectors.toMap(User::getName, user -> user));
//下面这样也行
// Map<String, User> nameToUser = userList.stream().collect(Collectors.toMap(User::getName, user -> {return user;}));
System.out.println(nameToUser);
//在映射时, 把年龄加1再返回
Map<String, User> nameToAge0 = userList.stream().collect(Collectors.toMap(User::getName, user -> { user.setAge(user.age+1); return user;}));
System.out.println(nameToAge0);
//那么如果分组的字段是一个非唯一的,比如state, 会报错
// Map<Integer, User> stateToUser = userList.stream().collect(Collectors.toMap(User::getState, user -> user));
// System.out.println(stateToUser);
//根据姓名为key, age为val组成 map<String, Ingeter>
//写法1
Map<String, Integer> nameToAge = userList.stream().collect(Collectors.toMap(User::getName, User::getAge));
System.out.println(nameToAge);
//写法2
Map<String, Integer> nameToAge2 = userList.stream().collect(Collectors.toMap(User::getName, user -> {return user.age;}));
System.out.println(nameToAge2);
Map<String, Integer> nameToAge3 = userList.stream().collect(Collectors.toMap(User::getName, user -> { user.setAge(user.age+1); return user.age;}));
System.out.println(nameToAge3);
}
/**
* 练习8 按名字去重
*/
@Test
public void test8() {
List<User> userList = generatorUserList();
Set<String> collect1 = userList.stream().distinct().map(User::getName).collect(Collectors.toSet());
System.out.println(collect1);
return;
}
/**
* 按名字统计数量
*/
@Test
public void test9() {
List<User> userList = generatorUserList();
Map<String, Long> collect = userList.stream().collect(Collectors.groupingBy(User::getName, Collectors.counting()));
System.out.println(collect);
return;
}
/**
* 求年龄最大值、最小值
*/
@Test
public void test10() {
List<User> userList = generatorUserList();
//如果有年龄一样的,会按list顺序取第一条, 所以除非能唯一, 否则多维度需要使用排序来获取值
Optional<User> min = userList.stream().min(Comparator.comparing(User::getAge));
System.out.println("min = " + min);
}
/**
* 获取某个字段的 最大值、最小值、求和、统计、计数
*/
@Test
public void test11() {
List<User> userList = generatorUserList();
IntSummaryStatistics collect = userList.stream().collect(Collectors.summarizingInt(User::getAge));
double average = collect.getAverage();
int max = collect.getMax();
int min = collect.getMin();
long sum = collect.getSum();
long count = collect.getCount();
System.out.println("collect = " + collect);
//单独对某个字段汇总
int sum1 = userList.stream().mapToInt(User::getAge).sum();
System.out.println("sum = " + sum1);
double avg1 = userList.stream().collect(Collectors.averagingDouble(User::getAge));
System.out.println("avg1 = " + avg1);
OptionalDouble avg2 = userList.stream().mapToDouble(User::getAge).average();
if (avg2.isPresent()) {
System.out.println("avg2 = " + avg2);
}
}
/**
* 按名字分组, 统计年龄相关的 计数, 总数,最大,最小,平均
*/
@Test
public void test12() {
List<User> userList = generatorUserList();
Map<String, IntSummaryStatistics> collect = userList.stream().collect(Collectors.groupingBy(User::getName, Collectors.summarizingInt(User::getAge)));
for(Map.Entry<String, IntSummaryStatistics> entry : collect.entrySet()) {
System.out.println(entry.getKey()+"-----"+entry.getValue());
}
}
/**
* 生成用户示例用户列表
* @return
*/
private List<User> generatorUserList() {
List<User> list = new ArrayList<>();
User user1 = new User();
user1.setName("19");
user1.setAge(18);
user1.setState(1);
user1.setCreateTime(new Date());
list.add(user1);
User user2 = new User();
user2.setName("1");
user2.setAge(22);
user2.setState(1);
user2.setCreateTime(new Date());
list.add(user2);
User user3 = new User();
user3.setName("2");
user3.setAge(15);
user3.setState(2);
user3.setCreateTime(new Date());
list.add(user3);
User user4 = new User();
user4.setName("23");
user4.setAge(15);
user4.setState(2);
user4.setCreateTime(new Date());
list.add(user4);
User user5 = new User();
user5.setName("23");
user5.setAge(18);
user5.setState(2);
user5.setCreateTime(new Date());
list.add(user5);
return list;
}
@Data
class User{
private String name;
private Integer age;
private Integer state;
private Date createTime;
}
}
/**
* public final class String
* String 是包装类型,且为final修饰不能被继承
* 一下提供字符串拼接和字符串反转的方法
*/
public class StringUtils {
public static void main(String[] args) {
StringUtils.stringJoin();
StringUtils.stringReverse();
}
public static void stringJoin(){
//StringBuffer字符串连接 线程安全的
StringBuffer buffer = new StringBuffer();
buffer.append("a");
buffer.append("b");
buffer.append("c");
buffer.append("d");
System.out.println(buffer.toString());
//StringBuilder字符串连接, 非线程安全的,但是速度快
StringBuilder builder = new StringBuilder();
builder.append("a");
builder.append("b");
builder.append("c");
builder.append("d");
System.out.println(builder.toString());
}
//字符串反转
public static void stringReverse(){
// StringBuffer reverse
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("abcdefg");
System.out.println(stringBuffer.reverse()); // gfedcba
// StringBuilder reverse
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("abcdefg");
System.out.println(stringBuilder.reverse()); // gfedcba
}
}
package cn.anzhongwei.lean.demo;
public class TestTryCatch {
public static void main(String[] args) {
System.out.println(getInt1());
System.out.println(getInt2());
}
public static int getInt1() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
/*
* return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
* 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
* 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
*/
} finally {
a = 40;
}
return a;
}
public static int getInt2() {
int a = 10;
try {
System.out.println(a / 0);
a = 20;
} catch (ArithmeticException e) {
a = 30;
return a;
/*
* return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了
* 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40
* 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30
*/
} finally {
a = 40;
return a; //如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40
}
// return a;
}
}
Java API规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现。hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。
Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。接下来,我们以spring-boot项目为例,介绍Spring Validation的使用。
如果spring-boot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖。如果spring-boot版本大于2.3.x,则需要手动引入依赖:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
对于web服务来说,为防止非法参数对业务造成影响,在Controller层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:
POST、PUT请求,使用requestBody传递参数;
GET请求,使用requestParam/PathVariable传递参数。
下面我们简单介绍下requestBody和requestParam/PathVariable的参数校验实战!
POST、PUT请求一般会使用requestBody传递参数,这种情况下,后端使用DTO对象进行接收。只要给DTO对象加上@Validated注解就能实现自动参数校验。比如,有一个保存User的接口,要求userName长度是2-10,account和password字段长度是6-20。
如果校验失败,会抛出MethodArgumentNotValidException异常,Spring默认会将其转为400(Bad Request)请求。
DTO表示数据传输对象(Data Transfer Object),用于服务器和客户端之间交互传输使用的。在spring-web项目中可以表示用于接收请求参数的Bean对象。
在DTO字段上声明约束注解
@Data
public class UserDTO {
private Long userId;
@NotNull
@Length(min = 2, max = 10)
private String userName;
@NotNull
@Length(min = 6, max = 20)
private String account;
@NotNull
@Length(min = 6, max = 20)
private String password;
}
在方法参数上声明校验注解
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}
这种情况下,使用@Valid和@Validated都可以。
GET请求一般会使用requestParam/PathVariable传参。如果参数比较多(比如超过6个),还是推荐使用DTO对象接收。
否则,推荐将一个个参数平铺到方法入参中。在这种情况下,必须在Controller类上标注@Validated注解,并在入参上声明约束注解(如@Min等)。如果校验失败,会抛出ConstraintViolationException异常。
代码示例如下:
@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
// 路径变量
@GetMapping("{userId}")
public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
// 校验通过,才会执行业务逻辑处理
UserDTO userDTO = new UserDTO();
userDTO.setUserId(userId);
userDTO.setAccount("11111111111111111");
userDTO.setUserName("xixi");
userDTO.setAccount("11111111111111111");
return Result.ok(userDTO);
}
// 查询参数
@GetMapping("getByAccount")
public Result getByAccount(@Length(min = 6, max = 20) @NotNull String account) {
// 校验通过,才会执行业务逻辑处理
UserDTO userDTO = new UserDTO();
userDTO.setUserId(10000000000000003L);
userDTO.setAccount(account);
userDTO.setUserName("xixi");
userDTO.setAccount("11111111111111111");
return Result.ok(userDTO);
}
}
前面说过,如果校验失败,会抛出MethodArgumentNotValidException或者ConstraintViolationException异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。
比如我们系统要求无论发送什么异常,http的状态码必须返回200,由业务码去区分系统的异常情况。
@RestControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
StringBuilder sb = new StringBuilder("校验失败:");
for (FieldError fieldError : bindingResult.getFieldErrors()) {
sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
}
String msg = sb.toString();
return Result.fail(BusinessCode.参数校验失败, msg);
}
@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleConstraintViolationException(ConstraintViolationException ex) {
return Result.fail(BusinessCode.参数校验失败, ex.getMessage());
}
}
在实际项目中,可能多个方法需要使用同一个DTO类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在DTO类的字段上加约束注解无法解决这个问题。因此,spring-validation支持了分组校验的功能,专门用来解决这类问题。
还是上面的例子,比如保存User的时候,UserId是可空的,但是更新User的时候,UserId的值必须>=10000000000000000L;其它字段的校验规则在两种情况下一样。这个时候使用分组校验的代码示例如下:
约束注解上声明适用的分组信息groups
@Data
public class UserDTO {
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String account;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String password;
/**
* 保存的时候校验分组
*/
public interface Save {
}
/**
* 更新的时候校验分组
*/
public interface Update {
}
}
@Validated注解上指定校验分组
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}
@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}
前面的示例中,DTO类里面的字段都是基本数据类型和String类型。但是实际场景中,有可能某个字段也是一个对象,这种情况先,可以使用嵌套校验。
比如,上面保存User信息的时候同时还带有Job信息。需要注意的是,此时DTO类的对应字段必须标记@Valid注解。
@Data
public class UserDTO {
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String account;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String password;
@NotNull(groups = {Save.class, Update.class})
@Valid
private Job job;
@Data
public static class Job {
@Min(value = 1, groups = Update.class)
private Long jobId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String jobName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String position;
}
/**
* 保存的时候校验分组
*/
public interface Save {
}
/**
* 更新的时候校验分组
*/
public interface Update {
}
}
嵌套校验可以结合分组校验一起使用。还有就是嵌套集合校验会对集合里面的每一项都进行校验,例如List<Job>字段会对这个list里面的每一个Job对象都进行校验
如果请求体直接传递了json数组给后台,并希望对数组中的每一项都进行参数校验。此时,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效!我们可以使用自定义list集合来接收参数:
包装List类型,并声明@Valid注解
public class ValidationList<E> implements List<E> {
@Delegate // @Delegate是lombok注解
@Valid // 一定要加@Valid注解
public List<E> list = new ArrayList<>();
// 一定要记得重写toString方法
@Override
public String toString() {
return list.toString();
}
}
@Delegate注解受lombok版本限制,1.18.6以上版本可支持。如果校验不通过,会抛出NotReadablePropertyException,同样可以使用统一异常进行处理。
比如,我们需要一次性保存多个User对象,Controller层的方法可以这么写:
@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) {
// 校验通过,才会执行业务逻辑处理
return Result.ok();
}
业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。
自定义spring validation非常简单,假设我们自定义加密id(由数字或者a-f的字母组成,32-256长度)校验,主要分为两步:
自定义约束注解
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface EncryptId {
// 默认错误消息
String message() default "加密id格式错误";
// 分组
Class<?>[] groups() default {};
// 负载
Class<? extends Payload>[] payload() default {};
}
实现ConstraintValidator接口编写约束校验器
public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {
private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 不为null才进行校验
if (value != null) {
Matcher matcher = PATTERN.matcher(value);
return matcher.find();
}
return true;
}
}
这样我们就可以使用@EncryptId进行参数校验了!
上面的示例都是基于注解来实现自动校验的,在某些情况下,我们可能希望以编程方式调用验证。这个时候可以注入javax.validation.Validator对象,然后再调用其api。
@Autowired
private javax.validation.Validator globalValidator;
// 编程式校验
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class);
// 如果校验通过,validate为空;否则,validate包含未校验通过项
if (validate.isEmpty()) {
// 校验通过,才会执行业务逻辑处理
} else {
for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) {
// 校验失败,做其它逻辑
System.out.println(userDTOConstraintViolation);
}
}
return Result.ok();
}
Spring Validation默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启Fali Fast模式,一旦校验失败就立即返回。
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失败模式
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
在spring-mvc中,RequestResponseBodyMethodProcessor是用于解析@RequestBody标注的参数以及处理@ResponseBody标注方法的返回值的。显然,执行参数校验的逻辑肯定就在解析参数的方法resolveArgument()中:
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
//将请求数据封装到DTO对象中
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
// 执行数据校验
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
}
可以看到,resolveArgument()调用了validateIfApplicable()进行参数校验。
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
// 获取参数注解,比如@RequestBody、@Valid、@Validated
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
// 先尝试获取@Validated注解
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
//如果直接标注了@Validated,那么直接开启校验。
//如果没有,那么判断参数前是否有Valid起头的注解。
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
//执行校验
binder.validate(validationHints);
break;
}
}
}
看到这里,大家应该能明白为什么这种场景下@Validated、@Valid两个注解可以混用。我们接下来继续看WebDataBinder.validate()实现。
@Override
public void validate(Object target, Errors errors, Object... validationHints) {
if (this.targetValidator != null) {
processConstraintViolations(
//此处调用Hibernate Validator执行真正的校验
this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
}
}
最终发现底层最终还是调用了Hibernate Validator进行真正的校验处理。
上面提到的将参数一个个平铺到方法参数中,然后在每个参数前面声明约束注解的校验方式,就是方法级别的参数校验。
实际上,这种方式可用于任何Spring Bean的方法上,比如Controller/Service等。其底层实现原理就是AOP,具体来说是通过MethodValidationPostProcessor动态注册AOP切面,然后使用MethodValidationInterceptor对切点方法织入增强。
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {
@Override
public void afterPropertiesSet() {
//为所有`@Validated`标注的Bean创建切面
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
//创建Advisor进行增强
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
//创建Advice,本质就是一个方法拦截器
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
}
接着看一下MethodValidationInterceptor:
public class MethodValidationInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//无需增强的方法,直接跳过
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
//获取分组信息
Class<?>[] groups = determineValidationGroups(invocation);
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<ConstraintViolation<Object>> result;
try {
//方法入参校验,最终还是委托给Hibernate Validator来校验
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
...
}
//有异常直接抛出
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
//真正的方法调用
Object returnValue = invocation.proceed();
//对返回值做校验,最终还是委托给Hibernate Validator来校验
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
//有异常直接抛出
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
}
实际上,不管是requestBody参数校验还是方法级别的校验,最终都是调用Hibernate Validator执行校验,Spring Validation只是做了一层封装。
空检查
@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY. 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) Validates that the annotated string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则 数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null
@Min 验证 Number 和 String 对象是否大等于指定的值
@Max 验证 Number 和 String 对象是否小等于指定的值
@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
@Digits 验证 Number 和 String 的构成是否合法 @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
@Range(min=, max=) 检查数字是否介于min和max之间.
@Range(min=10000,max=50000,message="range.bean.wage")
private BigDecimal wage;
@Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证)
@CreditCardNumber信用卡验证
@Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
@ScriptAssert(lang= ,script=, alias=)
@URL(protocol=,host=, port=,regexp=, flags=)
import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Data
public class User {
@NotNull(message = "主键不能为空", groups = {CheckGroup1.class})
private String id;
@Email(message = "邮箱格式不正确")
private String email;
@Size(min = 4, max = 16, message = "昵称长度为4~16个字符之间", groups = {CheckGroup1.class})
private String nickName;
@NotNull(message = "密码不能为空")
@Size(min = 8, max = 24, message = "密码长度为8~24个字符之间")
private String pwd;
}
import lombok.Data;
import lombok.ToString;
import java.text.MessageFormat;
import java.util.Map;
/**
* @Description 实体校验结果
*/
@Data
@ToString
public class ValidationResult {
/**
* 是否有异常
*/
private boolean hasErrors;
/**
* 异常消息记录
*/
private Map<String, String> errorMsg;
/**
* 获取异常消息组装
*
* @return
*/
public String getMessage() {
if (errorMsg == null || errorMsg.isEmpty()) {
return "";
}
StringBuilder message = new StringBuilder();
errorMsg.forEach((key, value) -> {
message.append(MessageFormat.format("{0}:{1} \r\n", key, value));
});
return message.toString();
}
}
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.commons.collections4.CollectionUtils;
import org.hibernate.validator.HibernateValidator;
import javax.validation.*;
import javax.validation.groups.Default;
/**
*
*/
public class ValidateUtils {
private ValidateUtils() {
}
// 线程安全的,直接构建也可以,这里使用静态代码块一样的效果
private static Validator validator;
static {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
public static <T> void validate(T t) {
Set<ConstraintViolation<T>> constraintViolations = validator.validate(t);
if (constraintViolations.size() > 0) {
StringBuilder validateError = new StringBuilder();
for (ConstraintViolation<T> constraintViolation : constraintViolations) {
validateError.append(constraintViolation.getMessage()).append(";");
}
throw new ValidationException(validateError.toString());
}
}
public static <T> void validate(T t, Class clazz) {
Set<ConstraintViolation<T>> constraintViolations = validator.validate(t, clazz);
if (constraintViolations.size() > 0) {
StringBuilder validateError = new StringBuilder();
for (ConstraintViolation<T> constraintViolation : constraintViolations) {
validateError.append(constraintViolation.getMessage()).append(";");
}
throw new ValidationException(validateError.toString());
}
}
public static <T> void validate1(T t) {
Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(false) // 为true,则有一个错误就结束校验
.buildValidatorFactory().getValidator();
Set<ConstraintViolation<T>> constraintViolations = validator.validate(t);
StringBuilder validateError = new StringBuilder();
for (ConstraintViolation<T> constraintViolation : constraintViolations) {
validateError.append(constraintViolation.getMessage()).append(";");
}
throw new ValidationException(validateError.toString());
}
// 自定义校验结果返回
/**
* 校验实体,返回实体所有属性的校验结果
*
* @param obj
* @param <T>
* @return
*/
public static <T> ValidationResult validateEntity(T obj) {
// 解析校验结果
Set<ConstraintViolation<T>> validateSet = validator.validate(obj, Default.class);
return buildValidationResult(validateSet);
}
/**
* 校验指定实体的指定属性是否存在异常
*
* @param obj
* @param propertyName
* @param <T>
* @return
*/
public static <T> ValidationResult validateProperty(T obj, String propertyName) {
Set<ConstraintViolation<T>> validateSet = validator.validateProperty(obj, propertyName, Default.class);
return buildValidationResult(validateSet);
}
/**
* 将异常结果封装返回
*
* @param validateSet
* @param <T>
* @return
*/
private static <T> ValidationResult buildValidationResult(Set<ConstraintViolation<T>> validateSet) {
ValidationResult validationResult = new ValidationResult();
if (!CollectionUtils.isEmpty(validateSet)) {
validationResult.setHasErrors(true);
Map<String, String> errorMsgMap = new HashMap<>();
for (ConstraintViolation<T> constraintViolation : validateSet) {
errorMsgMap.put(constraintViolation.getPropertyPath().toString(), constraintViolation.getMessage());
}
validationResult.setErrorMsg(errorMsgMap);
}
return validationResult;
}
public static <T> ValidationResult validateEntity1(T obj) {
ValidationResult result = new ValidationResult();
Set<ConstraintViolation<T>> set = validator.validate(obj, Default.class);
if (!CollectionUtils.isEmpty(set)) {
result.setHasErrors(true);
Map<String, String> errorMsg = new HashMap<String, String>();
for (ConstraintViolation<T> cv : set) {
errorMsg.put(cv.getPropertyPath().toString(), cv.getMessage());
}
result.setErrorMsg(errorMsg);
}
return result;
}
public static <T> ValidationResult validateEntity1(T obj, Class clazz) {
ValidationResult result = new ValidationResult();
Set<ConstraintViolation<T>> set = validator.validate(obj, clazz);
if (!CollectionUtils.isEmpty(set)) {
result.setHasErrors(true);
Map<String, String> errorMsg = new HashMap<String, String>();
for (ConstraintViolation<T> cv : set) {
errorMsg.put(cv.getPropertyPath().toString(), cv.getMessage());
}
result.setErrorMsg(errorMsg);
}
return result;
}
public static <T> ValidationResult validateProperty1(T obj, String propertyName) {
ValidationResult result = new ValidationResult();
Set<ConstraintViolation<T>> set = validator.validateProperty(obj, propertyName, Default.class);
if (!CollectionUtils.isEmpty(set)) {
result.setHasErrors(true);
Map<String, String> errorMsg = new HashMap<String, String>();
for (ConstraintViolation<T> cv : set) {
errorMsg.put(propertyName, cv.getMessage());
}
result.setErrorMsg(errorMsg);
}
return result;
}
}
public interface CheckGroup1 {
}
import java.util.Map;
public class Client {
public static void main(String[] args) {
User user = new User();
ValidationResult validRes = ValidateUtils.validateEntity1(user, CheckGroup1.class);
if (validRes.isHasErrors()) {
for (Map.Entry<String, String> entry : validRes.getErrorMsg().entrySet()) {
System.out.println(entry.getValue());
}
}
}
}
package cn.anzhongwei.lean.demo.zip;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ZipUtilFileInfo {
private String name;
private String url;
private Boolean Local;
}
package cn.anzhongwei.lean.demo.zip;
import org.apache.commons.lang.ArrayUtils;
import org.apache.pdfbox.io.IOUtils;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.zip.*;
public class ZipUtils2 {
private static final int BUFFER = 256;
public static void main(String[] args) throws IOException {
String hallFilePath = "d:/" + "qwe";
compress(Paths.get(hallFilePath).toString(), hallFilePath + ".zip");
List<ZipUtilFileInfo> downFileInfos = new ArrayList<>();
ZipUtilFileInfo d1 = new ZipUtilFileInfo( "价格导入模板.xlsx", "http://网络地址", false);
ZipUtilFileInfo d2 = new ZipUtilFileInfo( "订单导入_仓储.xlsx", "http://网络地址", false);
downFileInfos.add(d1);
downFileInfos.add(d2);
File zipfile1 = gerenatorZIPFileFromUrl(downFileInfos, "t1.zip");
File zipfile2 = gerenatorZIPFileFromUrl(downFileInfos, "t2.zip");
File zipfile3 = gerenatorZIPFileFromUrl(downFileInfos, "t3.zip");
List<ZipUtilFileInfo> downFileInfos2 = new ArrayList<>();
ZipUtilFileInfo dd1 = new ZipUtilFileInfo( zipfile1.getName(), zipfile1.getAbsolutePath(), true);
ZipUtilFileInfo dd2 = new ZipUtilFileInfo( zipfile2.getName(), zipfile1.getAbsolutePath(), true);
ZipUtilFileInfo dd3 = new ZipUtilFileInfo( zipfile3.getName(), zipfile1.getAbsolutePath(), true);
downFileInfos2.add(dd1);
downFileInfos2.add(dd2);
downFileInfos2.add(dd3);
File ff2 = gerenatorZIPFileFromUrl(downFileInfos2, "tt1.zip");
System.out.println(zipfile1.getAbsolutePath());
InputStream inputStream = new FileInputStream(ff2);
File tempFile = new File("C:\\testFile\\");
if (!tempFile.exists()) {
tempFile.mkdirs();
}
OutputStream os = new FileOutputStream(tempFile.getPath() + File.separator + "shishi.zip");
byte[] bs = new byte[1024];
int len;
while ((len = inputStream.read(bs)) != -1) {
os.write(bs, 0, len);
}
}
//文件或目录打包成zip
public static void compress(String fromPath, String toPath) throws IOException {
File fromFile = new File(fromPath);
File toFile = new File(toPath);
if (!fromFile.exists()) {
throw new RuntimeException(fromPath + "不存在!");
}
try (FileOutputStream outputStream = new FileOutputStream(toFile);
CheckedOutputStream checkedOutputStream = new CheckedOutputStream(outputStream, new CRC32());
ZipOutputStream zipOutputStream = new ZipOutputStream(checkedOutputStream)) {
String baseDir = "";
compress(fromFile, zipOutputStream, baseDir);
}
}
private static void compress(File file, ZipOutputStream zipOut, String baseDir) throws IOException {
if (file.isDirectory()) {
compressDirectory(file, zipOut, baseDir);
} else {
compressFile(file, zipOut, baseDir);
}
}
private static void compressFile(File file, ZipOutputStream zipOut, String baseDir) throws IOException {
if (!file.exists()) {
return;
}
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
ZipEntry entry = new ZipEntry(baseDir + file.getName());
zipOut.putNextEntry(entry);
int count;
byte[] data = new byte[BUFFER];
while ((count = bis.read(data, 0, BUFFER)) != -1) {
zipOut.write(data, 0, count);
}
}
}
private static void compressDirectory(File dir, ZipOutputStream zipOut, String baseDir) throws IOException {
File[] files = dir.listFiles();
if (files != null && ArrayUtils.isNotEmpty(files)) {
for (File file : files) {
compress(file, zipOut, baseDir + dir.getName() + File.separator);
}
}
}
private static File gerenatorZIPFileFromUrl(List<ZipUtilFileInfo> downFileInfos, String zipFileName) {
try {
File zipFile = File.createTempFile(zipFileName, ".zip");
FileOutputStream f = new FileOutputStream(zipFile);
CheckedOutputStream csum = new CheckedOutputStream(f, new Adler32());
ZipOutputStream zos = new ZipOutputStream(csum);
for (ZipUtilFileInfo downFileInfo : downFileInfos) {
InputStream inputStream;
if (Objects.isNull(downFileInfo.getLocal())) {
throw new RuntimeException("url类型未指定是否本地,请检查代码");
}
if (downFileInfo.getLocal()) {
inputStream = new FileInputStream(downFileInfo.getUrl());
} else {
inputStream = getStreamByUrl(downFileInfo.getUrl());
}
zos.putNextEntry(new ZipEntry(downFileInfo.getName()));
int bytesRead = 0;
// 向压缩文件中输出数据
while((bytesRead = inputStream.read()) != -1){
zos.write(bytesRead);
}
inputStream.close();
zos.closeEntry();
}
zos.close();
return zipFile;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static InputStream getStreamByUrl(String strUrl){
HttpURLConnection conn = null;
try {
URL url = new URL(strUrl);
conn = (HttpURLConnection)url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(20 * 1000);
final ByteArrayOutputStream output = new ByteArrayOutputStream();
IOUtils.copy(conn.getInputStream(),output);
return new ByteArrayInputStream(output.toByteArray());
} catch (Exception e) {
e.printStackTrace();
}finally {
try{
if (conn != null) {
conn.disconnect();
}
}catch (Exception e){
e.printStackTrace();
}
}
return null;
}
}
package cn.anzhongwei.lean.demo.list.arraylist;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Demo {
public static void main(String[] args) {
Object obj = new Object();
List<Object> list = new ArrayList<>();
int objCount = 1000;
for (int i = 0; i < objCount; i++) {
list.add(obj);
}
int testTimes = 5;
for (int i = 0; i < testTimes; i++) {
testFor(list);
testForEnhanced(list);
testForEach(list);
testIterator(list);
System.out.println("\n");
}
System.out.println("---------------------------------------------------------\n");
List<Object> list2 = new ArrayList<>();
for (int i = 0; i < objCount; i++) {
list2.add(obj);
}
for (int i = 0; i < testTimes; i++) {
testFor(list2);
testForEnhanced(list2);
testForEach(list2);
testIterator(list2);
System.out.println();
}
}
private static void testFor(List<Object> list) {
long startTime = 0L;
long endTime = 0L;
startTime = System.nanoTime();
for (int i = 0; i < list.size(); i++) {
Object o = list.get(i);
}
endTime = System.nanoTime();
System.out.println("for所用时间(ns) :" + (endTime - startTime));
}
private static void testForEnhanced(List<Object> list) {
long startTime = 0L;
long endTime = 0L;
startTime = System.nanoTime();
for (Object o : list) {
Object value = o;
}
endTime = System.nanoTime();
System.out.println("增强for所用时间(ns) :" + (endTime - startTime));
}
private static void testForEach(List<Object> list) {
long startTime = 0L;
long endTime = 0L;
startTime = System.nanoTime();
list.forEach(o->{Object obj = o;});
endTime = System.nanoTime();
System.out.println("forEach所用时间(ns) :" + (endTime - startTime));
}
private static void testIterator(List<Object> list) {
long startTime = 0L;
long endTime = 0L;
startTime = System.nanoTime();
Iterator<Object> iterator = list.iterator();
while (iterator.hasNext()) {
Object o = iterator.next();
}
endTime = System.nanoTime();
System.out.println("iterator所用时间(ns) :" + (endTime - startTime));
}
}
package cn.anzhongwei.lean.demo.map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class MapTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");
//第一种:普遍使用,二次取值
System.out.println("通过Map.keySet遍历key和value:");
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}
//第二种
System.out.println("通过Map.entrySet使用iterator遍历key和value:");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第三种:推荐,尤其是容量大时
System.out.println("通过Map.entrySet遍历key和value");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第四种
System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
for (String v : map.values()) {
System.out.println("value= " + v);
}
}
}
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 从mybatis的sql日志自动填充sql占位符
*/
public class SqlReplaceUtil extends JFrame {
private static final Pattern HUMP_PATTERN = Pattern.compile("\\?");
JPanel jp;
JButton b1;
JTextArea sqlTextField;
JTextArea paramTextField;
JTextArea resultTextField;
JLabel lb3;
JLabel lb4;
SqlReplaceUtil() {
jp = new JPanel();
b1 = new JButton("替换");
sqlTextField = new JTextArea("输入待处理的SQL,比如:insert into test (id,name,age) values (?,?,?)",10,90);
paramTextField = new JTextArea("输入待处理参数,比如:100(Integer),zhangsan(String),null",10,90);
resultTextField = new JTextArea(10,90);
lb3 = new JLabel("结果为:");
lb4 = new JLabel("");
b1.addActionListener(new ActionListener() {//响应按钮的事件
public void actionPerformed(ActionEvent e) {
try {
lb4.setText("");
String sql = sqlTextField.getText();
String param = paramTextField.getText();
resultTextField.setText(replaceSql(sql, param));
} catch (Exception ex) {
lb4.setText(ex.getMessage());
}
}
});
jp.add(sqlTextField);
jp.add(paramTextField);
jp.add(b1);
jp.add(resultTextField);
jp.add(lb4);
add(jp);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setLocation(600, 200);
setVisible(true);
pack();
setSize(1050, 700);
setLocationRelativeTo(null);
setVisible(true);
setResizable(false);
}
public static void main(String[] args) {
SqlReplaceUtil e = new SqlReplaceUtil();
Container contentPane = e.getContentPane();
contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS));
}
private String replaceSql(String sql, String param) {
String[] split = param.split(",");
//校验数量
int paramCount = split.length;
int matcherCount = 0;
Matcher matcher = HUMP_PATTERN.matcher(sql);
while (matcher.find()) {
matcherCount++;
}
if (paramCount != matcherCount) {
throw new RuntimeException("待替换参数和参数数量不一致");
}
//处理参数
for (int i = 0; i < split.length; i++) {
split[i] = split[i].trim();
int index = split[i].lastIndexOf('(');
if (split[i].equals("null")||index<0) {
continue;
}
split[i] = "'" + split[i].substring(0, index) + "'";
}
StringBuffer sb = new StringBuffer();
Matcher matcher2 = HUMP_PATTERN.matcher(sql);
int index = 0;
while (matcher2.find()) {
matcher2.appendReplacement(sb, split[index]);
index++;
}
matcher2.appendTail(sb);
return sb.toString();
}
}
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<!-- 我用的最新的, 一般1.14.0以上都没问题 -->
<version>1.15.1</version>
</dependency>
public class TargetClass {
public void performAction() {
System.out.println("原始方法: performAction");
}
public String sayHello(String name) {
return "Hello, " + name;
}
}
import net.bytebuddy.implementation.bind.annotation.*;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
/**
* 通用方法拦截器
*/
public class GenericInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method,
@AllArguments Object[] args,
@This Object obj,
@SuperCall Callable<?> callable) throws Exception {
// 获取动态添加的字段值
String proxyField = (String) obj.getClass().getField("proxyField").get(obj);
System.out.println("[拦截] proxyField 值: " + proxyField);
// 修改字段值
System.out.println("[通用拦截] 修改字段proxyField值: Modified by interceptor" );
obj.getClass().getField("proxyField").set(obj, "Modified by interceptor");
System.out.println("[通用拦截] 方法开始: " + method.getName());
Object result = callable.call(); // 执行原始方法
System.out.println("[通用拦截] 方法结束: " + method.getName());
return result;
}
}
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 通用 Byte Buddy 动态代理工具类
*/
public class ByteBuddyProxy {
/**
* 创建一个代理对象
*
* @param targetClass 目标类类型
* @param interceptor 拦截器类(需使用 Byte Buddy 注解定义逻辑)
* @param <T> 泛型
* @return 代理实例
* @throws NoSuchMethodException
* @throws IllegalAccessException
* @throws InvocationTargetException
* @throws InstantiationException
*/
public static <T> T createProxy(Class<T> targetClass, Class<?> interceptor) throws Exception {
Class<?> dynamicType = new ByteBuddy()
.subclass(targetClass)
// ElementMatchers.any() 拦截所有方法
.method(ElementMatchers.any())
// MethodDelegation.to(interceptor) 拦截器类 方法增强
.intercept(MethodDelegation.to(interceptor))
// 动态的添加字段。这里可以放到参数里面,然后由调用方传递进来要添加什么参数
.defineField("proxyField", String.class, Visibility.PUBLIC) // 可选:添加字段
.defineField("abc", String.class, Visibility.PUBLIC) // 可选:添加字段
.make()
.load(targetClass.getClassLoader())
.getLoaded();
// 支持构造实例
// .getDeclaredConstructor()
// .newInstance();
Constructor<?> constructor = dynamicType.getDeclaredConstructor();
Object instance = constructor.newInstance();
// 现在就可以 调用方法触发拦截逻辑 了
// ((TargetClass) instance).performAction();
// 设置字段值
System.out.println("[通用拦截] 设置字段proxyField值: Hello Dynamic Field" );
dynamicType.getField("proxyField").set(instance, "Hello Dynamic Field");
System.out.println("[通用拦截] 设置字段abc值: def" );
dynamicType.getField("abc").set(instance, "def");
return (T) instance;
}
}
public class ByteBuddyGenericExample {
public static void main(String[] args) throws Exception {
// 创建代理对象
TargetClass proxyInstance = ByteBuddyProxy
.createProxy(TargetClass.class, GenericInterceptor.class);
// 调用方法
proxyInstance.performAction();
String result = proxyInstance.sayHello("World");
System.out.println("返回值: " + result);
String abc = (String) proxyInstance.getClass().getField("abc").get(proxyInstance);
System.out.println("last在打印一遍proxyField: " + abc);
}
}
maven项目需要添加依赖
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
*net.sf.cglib.core *底层字节码处理类。
*net.sf.cglib.transform *该包中的类用于class文件运行时转换或编译时转换。
*net.sf.cglib.proxy *该包中的类用于创建代理和方法拦截。
*net.sf.cglib.reflect *该包中的类用于快速反射,并提供了C#风格的委托。
*net.sf.cglib.util *集合排序工具类。
*net.sf.cglib.beans *JavaBean工具类。
cglib是通过动态的生成一个子类去覆盖所要代理类的非final方法,并设置好callback,则原有类的每个方法调用就会转变成调用用户定义的拦截方法(interceptors)。
CGLIB代理相关的常用API如下图所示:

net.sf.cglib.proxy.Callback接口在CGLIB包中是一个重要的接口,所有被net.sf.cglib.proxy.Enhancer类调用的回调(callback)接口都要继承这个接口。
net.sf.cglib.proxy.MethodInterceptor能够满足任何的拦截(interception )需要。对有些情况下可能过度。为了简化和提高性能,CGLIB包提供了一些专门的回调(callback)类型:
CGLIB动态代理的原理就是用Enhancer生成一个原有类的子类,并且设置好callback到proxy, 则原有类的每个方法调用都会转为调用实现了MethodInterceptor接口的proxy的intercept() 函数,如图

在intercept()函数里,除执行代理类的原因方法,在原有方法前后加入其他需要实现的过程,改变原有方法的参数值,即可以实现对原有类的代理了。这似于AOP中的around advice。
当对代理中所有方法的调用时,都会转向MethodInterceptor类型的拦截(intercept)方法,在拦截方法中再调用底层对象相应的方法。下面我们举个例子,假设你想对目标对象的所有方法调用进行权限的检查,如果没有经过授权,就抛出一个运行时的异常。
net.sf.cglib.proxy.MethodInterceptor接口是最通用的回调(callback)类型,它经常被基于代理的AOP用 来实现拦截(intercept)方法的调用。
MethodInterceptor接口只定义了一个方法:
public Object intercept(Object object, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable;
参数Object object是被代理对象,不会出现死循环的问题。
参数java.lang.reflect.Method method是java.lang.reflect.Method类型的被拦截方法。
参数Object[] args是被被拦截方法的参数。
参数MethodProxy proxy是CGLIB提供的MethodProxy 类型的被拦截方法。
注意:
1、若原方法的参数存在基本类型,则对于第三个参数Object[] args会被转化成类的类型。如原方法的存在一个参数为int,则在intercept方法中,对应的会存在一个Integer类型的参数。
2、若原方法为final方法,则MethodInterceptor接口无法拦截该方法。
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
class MethodInterceptorImpl implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before invoke " + method);
Object result = proxy.invokeSuper(obj, args);
System.out.println("After invoke" + method);
return result;
}
}
Object result=proxy.invokeSuper(o,args); 表示调用原始类的被拦截到的方法。这个方法的前后添加需要的过程。在这个方法中,我们可以在调用原方法之前或之后注入自己的代码。
由于性能的原因,对原始方法的调用使用CGLIB的net.sf.cglib.proxy.MethodProxy对象,而不是反射中一般使用java.lang.reflect.Method对象。
net.sf.cglib.proxy.Enhancer中有几个常用的方法:
注意:在参数中,基本类型应被转化成类的类型。
基本代码:
public Object createProxy(Class targetClass) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetClass);
enhancer.setCallback(new MethodInterceptorImpl ());
return enhancer.create();
}
createProxy方法返回值是targetClass的一个实例的代理。
例1:使用CGLIB生成代理的基本使用。
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class TestMain {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Cglib.class);
enhancer.setCallback(new HelloProxy());
Cglib cglibProxy = (Cglib)enhancer.create();
cglibProxy.cglib();
}
}
class Cglib{
public void cglib(){
System.out.println("CGLIB");
}
}
class HelloProxy implements MethodInterceptor{
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Hello");
Object object = proxy.invokeSuper(obj, args);
System.out.println("Powerful!");
return object;
}
}
输出内容:
Hello
CGLIB
Powerful!
例2:使用CGLIB创建一个Dao工厂,并展示一些基本特性。
public interface Dao {
void add(Object o);
void add(int i);
void add(String s);
}
public class DaoImpl implements Dao {
@Override
public void add(Object o) {
System.out.println("add(Object o)");
}
@Override
public void add(int i) {
System.out.println("add(int i)");
}
public final void add(String s) {
System.out.println("add(String s)");
}
}
public class Proxy implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("拦截前...");
// 输出参数类型
for (Object arg : args) {
System.out.print(arg.getClass() + ";");
}
Object result = proxy.invokeSuper(obj, args);
System.out.println("拦截后...");
return result;
}
}
public class DaoFactory {
public static Dao create() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(DaoImpl.class);
enhancer.setCallback(new Proxy());
Dao dao = (Dao) enhancer.create();
return dao;
}
}
public class TestMain {
public static void main(String[] args) {
Dao dao = DaoFactory.create();
dao.add(new Object());
dao.add(1);
dao.add("1");
}
}
输出内容:
拦截前...
class java.lang.Object;add(Object o)
拦截后...
拦截前...
class java.lang.Integer;add(int i)
拦截后...
add(String s)
net.sf.cglib.proxy.CallbackFilter有选择的对一些方法使用回调。
CallbackFilter可以实现不同的方法使用不同的回调方法。所以CallbackFilter称为"回调选择器"更合适一些。
CallbackFilter中的accept方法,根据不同的method返回不同的值i,这个值是在callbacks中callback对象的序号,就是调用了callbacks[i]。
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import net.sf.cglib.proxy.NoOp;
public class CallbackFilterDemo {
public static void main(String[] args) {
// 回调实例数组
Callback[] callbacks = new Callback[]{new MethodInterceptorImpl(), NoOp.INSTANCE};
// 使用enhancer,设置相关参数。
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(User.class);
enhancer.setCallbacks(callbacks);
enhancer.setCallbackFilter(new CallbackFilterImpl());
// 产生代理对象
User proxyUser = (User) enhancer.create();
proxyUser.pay(); // 买
proxyUser.eat(); // 吃
}
}
/**
* 回调过滤器类。
*/
public class CallbackFilterImpl implements CallbackFilter {
@Override
public int accept(Method method) {
String methodName = method.getName();
if ("eat".equals(methodName)) {
return 1; // eat()方法使用callbacks\[1\]对象拦截。
} else if ("pay".equals(methodName)) {
return 0; // pay()方法使用callbacks\[0\]对象拦截。
}
return 0;
}
}
/**
* 自定义回调类。
*/
public class MethodInterceptorImpl implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before invoke " + method);
Object result = proxy.invokeSuper(obj, args); // 原方法调用。
System.out.println("After invoke" + method);
return result;
}
}
class User {
public void pay() {
System.out.println("买东西");
}
public void eat() {
System.out.println("吃东西");
}
}
输出结果:
Before invoke public void sjq.cglib.filter.User.pay()
pay()
After invokepublic void sjq.cglib.filter.User.pay()
eat()
CGLIB的代理包net.sf.cglib.proxy.Mixin类提供对Minix编程的支持。Minix允许多个对象绑定到一个单个的大对象上。在代理中对方法的调用委托到下面相应的对象中。 这是一种将多个接口混合在一起的方式 , 实现了多个接口。
Minix是一种多继承的替代方案, 很大程度上解决了多继承的很多问题 , 实现和理解起来都比较容易。
import net.sf.cglib.proxy.Mixin;
public class MixinDemo {
public static void main(String[] args) {
//接口数组
Class<?>[] interfaces = new Class[] { MyInterfaceA.class, MyInterfaceB.class };
//实例对象数组
Object[] delegates = new Object[] { new MyInterfaceAImpl(), new MyInterfaceBImpl() };
//Minix组合为o对象。
Object o = Mixin.create(interfaces, delegates);
MyInterfaceA a = (MyInterfaceA) o;
a.methodA();
MyInterfaceB b = (MyInterfaceB) o;
b.methodB();
System.out.println("\\r\\n 输出Mixin对象的结构...");
Class clazz = o.getClass();
Method[] methods = clazz.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
System.out.println(methods[i].getName());
}
System.out.println(clazz);
}
}
interface MyInterfaceA {
public void methodA();
}
interface MyInterfaceB {
public void methodB();
}
class MyInterfaceAImpl implements MyInterfaceA {
@Override
public void methodA() {
System.out.println("MyInterfaceAImpl.methodA()");
}
}
class MyInterfaceBImpl implements MyInterfaceB {
@Override
public void methodB() {
System.out.println("MyInterfaceBImpl.methodB()");
}
}
输出结果:
MyInterfaceAImpl.methodA()
MyInterfaceBImpl.methodB()
输出Mixin对象的结构...
methodA
methodB
newInstance
class sjq.cglib.mixin.MyInterfaceA
M ixin ByC GLI B MixinByCGLIB
d1f6261a
package sjq.cglib.bean.copy;
import net.sf.cglib.beans.BeanCopier;
public class PropertyCopyDemo {
public static void main(String[] args) {
//两个对象
Other other = new Other("test", "1234");
Myth myth = new Myth();
System.out.println(other);
System.out.println(myth);
//构建BeanCopier,并copy对象的属性值。
BeanCopier copier = BeanCopier.create(Other.class, Myth.class, false);
copier.copy(other, myth, null);
System.out.println(other);
System.out.println(myth);
}
}
class Other {
private String username;
private String password;
private int age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Other(String username, String password) {
super();
this.username = username;
this.password = password;
}
@Override
public String toString() {
return "Other: " + username + ", " + password + ", " + age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Myth {
private String username;
private String password;
private String remark;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "Myth: " + username + ", " + password + ", " + remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getRemark() {
return remark;
}
}
运行结果如下:
Other: test, 1234, 0
Myth: null, null, null
Other: test, 1234, 0
Myth: test, 1234, null
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import net.sf.cglib.beans.BeanGenerator;
import net.sf.cglib.beans.BeanMap;
/**
* 动态实体bean
*/
public class CglibBean {
/**
* 实体Object
*/
public Object object = null;
/**
* 属性map
*/
public BeanMap beanMap = null;
public CglibBean() {
super();
}
@SuppressWarnings("unchecked")
public CglibBean(Map<String, Class> propertyMap) {
this.object = generateBean(propertyMap);
this.beanMap = BeanMap.create(this.object);
}
/**
* 给bean属性赋值
* @param property属性名
* @param value值
*/
public void setValue(String property, Object value) {
beanMap.put(property, value);
}
/**
* 通过属性名得到属性值
* @param property属性名
*/
public Object getValue(String property) {
return beanMap.get(property);
}
/**
* 得到该实体bean对象。
*/
public Object getObject() {
return this.object;
}
/**
* 生成Bean
* @param propertyMap
* @return
*/
@SuppressWarnings("unchecked")
private Object generateBean(Map<String, Class> propertyMap) {
BeanGenerator generator = new BeanGenerator();
Set keySet = propertyMap.keySet();
for (Iterator i = keySet.iterator(); i.hasNext();) {
String key = (String) i.next();
generator.addProperty(key, (Class) propertyMap.get(key));
}
return generator.create();
}
}
测试并使用动态Bean
import java.lang.reflect.Method;
import java.util.HashMap;
/**
* Cglib测试类
*/
public class CglibTest {
@SuppressWarnings("unchecked")
public static void main(String[] args) throws ClassNotFoundException {
// 设置类成员属性
HashMap<String, Class> propertyMap = new HashMap<String, Class>();
propertyMap.put("id", Class.forName("java.lang.Integer"));
propertyMap.put("name", Class.forName("java.lang.String"));
propertyMap.put("address", Class.forName("java.lang.String"));
// 生成动态Bean
CglibBean bean = new CglibBean(propertyMap);
// 给Bean设置值
bean.setValue("id", new Integer(123));
bean.setValue("name", "454");
bean.setValue("address", "789");
// 从Bean中获取值,当然了获得值的类型是Object
System.out.println(">>id=" + bean.getValue("id"));
System.out.println(">>name=" + bean.getValue("name"));
System.out.println(">>address=" + bean.getValue("address"));// 获得bean的实体
Object object = bean.getObject();
// 通过反射查看所有方法名
Class clazz = object.getClass();
Method[] methods = clazz.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
System.out.println(methods[i].getName());
}
}
}
输出:
\>>id=123
\>>name=454
\>>address=789
setId
getAddress
getName
getId
setName
setAddress
class net.sf.cglib.empty.Object
B eanG ener ator ByC GLI B BeanGeneratorByCGLIB
1d39cfaa
public interface IRegisterService {
void register(String name, String pwd);
}
public class RegisterServiceImpl implements IRegisterService {
@Override
public void register(String name, String pwd) {
System.out.println(String.format("【向数据库中插入数据】name:%s,pwd:%s", name, pwd));
}
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
class InsertDataHandler implements InvocationHandler {
// 持有一个被代理类的引用, 但是这个被代理类不是具体的类。而是超类
Object obj;
public Object getProxy(Object obj){
this.obj = obj;
return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
doBefore();
// 在这里调用被代理的方法
Object result = method.invoke(obj, args);
doAfter();
return result;
}
private void doBefore() {
System.out.println("[Proxy]一些前置处理");
}
private void doAfter() {
System.out.println("[Proxy]一些后置处理");
}
}
public class DynamicProxy {
public static void main(String[] args) {
// 实例化被代理类
IRegisterService iRegisterService = new RegisterServiceImpl();
// 实例化代理类
InsertDataHandler insertDataHandler = new InsertDataHandler();
// 通过Proxy获取代理类
IRegisterService proxy = (IRegisterService)insertDataHandler.getProxy(iRegisterService);
// 执行方法
proxy.register("RyanLee", "123");
}
}
public interface IRegisterService {
void register(String name, String pwd);
}
public class RegisterServiceImpl implements IRegisterService {
@Override
public void register(String name, String pwd) {
System.out.println(String.format("【向数据库中插入数据】name:%s,pwd:%s", name, pwd));
}
}
class RegisterServiceProxy implements IRegisterService {
// 代理类持有被代理类对象的引用
IRegisterService iRegisterService;
public RegisterServiceProxy(IRegisterService iRegisterService) {
this.iRegisterService = iRegisterService;
}
@Override
public void register(String name, String pwd) {
System.out.println("[Proxy]一些前置处理");
//这里调用被代理类的方法,也是静态指定的
iRegisterService.register(name, pwd);
System.out.println("[Proxy]一些后置处理");
}
}
public class StaticProxy {
public static void main(String[] args) {
IRegisterService iRegisterService = new RegisterServiceImpl();
IRegisterService proxy = new RegisterServiceProxy(iRegisterService);
proxy.register("RyanLee", "123");
}
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* description: Java Builder模式练习
* @version v1.0
* @author w
* @date 2021年7月7日上午11:37:49
**/
@AllArgsConstructor
@Data
@NoArgsConstructor
public class User {
private String id ;
private String name ;
private Integer age ;
public static Builder builder(){
return new Builder();
}
public static class Builder{
private String id ;
private String name ;
private Integer age ;
public Builder id(String id) {
this.id = id ;
return this;
}
public Builder name(String name) {
this.name = name ;
return this ;
}
public Builder age(Integer age) {
this.age = age ;
return this;
}
public User build() {
return new User(this);
}
public User build2() {
return new User(this.id , this.name , this.age);
}
}
public User(Builder builder) {
this.id = builder.id;
this.name = builder.name ;
this.age = builder.age;
}
}
public class TestBuilder {
public static void main(String[] args) {
User user = User.builder().id("1").name("xiaoming").age(18).build();
System.out.println(user);
}
}
import java.io.IOException;
/**
* 方法一: 继承Thread
*/
public class M01ExtentThread {
public static void main(String[] args) throws IOException {
M01ExtentThread test = new M01ExtentThread();
MyThread thread1 = test.new MyThread();
MyThread thread2 = test.new MyThread();
thread1.setName("线程1");
thread2.setName("线程2");
thread1.start();
thread2.start();
System.out.println("主线程输出");
}
class MyThread extends Thread{
@Override
public void run() {
for(int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+"线程输出"+i);
}
}
}
}
/**
* 方法2实现Runnable接口
*/
public class M02ImplentRunnable {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
for(int i = 0; i < 3; i++) {
System.out.println("runnablex线程输出"+i);
}
}
};
new Thread(runnable).start();
System.out.println("主线程输出");
}
}
package cn.anzhongwei.lean.demo.thread;
import java.util.Random;
import java.util.concurrent.*;
/**
* 和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。
*
* call()方法可以有返回值
*
* call()方法可以声明抛出异常
*
* Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口。
* 因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。
* public interface Future<V> {
* //视图取消该Future里面关联的Callable任务
* boolean cancel(boolean mayInterruptIfRunning);
* //如果在Callable任务正常完成前被取消,返回True
* boolean isCancelled();
* //若Callable任务完成,返回True
* boolean isDone();
* //返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
* V get() throws InterruptedException, ExecutionException;
* //返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
* V get(long timeout, TimeUnit unit)
* throws InterruptedException, ExecutionException, TimeoutException;
* }
*
* 介绍了相关的概念之后,创建并启动有返回值的线程的步骤如下:
*
* 1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
*
* 2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
*
* 3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
*
* 4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
*/
public class M03ImplementCallable01 {
public static void main(String[] args) {
//1. 此处使用的是匿名类方式
// Callable<String> callable = new Callable<String>() {
// public String call() throws Exception {
// for(int i = 0; i < 3; i++) {
// System.out.println("callable线程输出"+i);
// }
// return "返回随机数"+(new Random().nextInt(100));
// }
// };
//2. 使用静态内部类
Callable<String> callable = new InnerStaticCallable();
//3. 使用外部实现类。略
FutureTask<String> future = new FutureTask<String>(callable);
Thread thread = new Thread(future);
thread.start();
try {
String resI = future.get();
System.out.println(resI);
} catch (InterruptedException ie) {
ie.printStackTrace();
} catch (ExecutionException ee) {
ee.printStackTrace();
}
//此种方法可以用, 但是不明确应用场景, 返回资源的值是在new FutureTask中传入的,
//runnable接口实现直接使用Thread的start方法运行就行,传到FutureTask中有啥用
Runnable runnable = new Runnable() {
@Override
public void run() {
for(int i = 0; i < 3; i++) {
System.out.println("Runnable线程输出"+i);
}
}
};
FutureTask<Integer> target = new FutureTask<>(runnable, 12);
Thread thread1 = new Thread(target);
thread1.start();
try {
Integer resI1 = target.get();
System.out.println(resI1);
} catch (InterruptedException ie) {
ie.printStackTrace();
} catch (ExecutionException ee) {
ee.printStackTrace();
}
System.out.println("主线程输出");
}
//也可以使用内部类方式
static class InnerStaticCallable implements Callable<String> {
@Override
public String call() {
for(int i = 0; i < 3; i++) {
System.out.println("callable线程输出"+i);
}
return "返回随机数"+(new Random().nextInt(100));
}
}
}
import java.util.concurrent.*;
/**
* 1.5后引入的Executor框架的最大优点是把任务的提交和执行解耦。
* 要执行任务的人只需把Task描述清楚,然后提交即可。
* 这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。
* 具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。
* Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。
* 因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,
* 还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象用Executor在构造器中。
*
* 关于ExecutorService执行submit和execute的区别
* 1、接收的参数不一样
* 2、submit有返回值,而execute没有
* 3、submit方便Exception处理
* 如果你在你的task里会抛出checked或者unchecked exception,而你又希望外面的调用者能够感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。
*/
public class M04ExccutorCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService1 = Executors.newSingleThreadExecutor();
ExecutorService executorService2 = Executors.newSingleThreadExecutor();
Runnable runnable = new Runnable(){
@Override
public void run() {
for(int i = 0; i < 3; i++) {
System.out.println("Runnable接口"+i);
}
}
};
//执行Runnable接口,无返回值
executorService1.execute(runnable);
executorService1.shutdown();
//执行Callable接口,有返回值
Callable<String> stringCallable = new Callable(){
@Override
public String call() throws Exception {
for(int i = 0; i < 3; i++) {
System.out.println("Callable接口"+i);
}
return "Hello World";
}
};
Future<String> submit2 = executorService2.submit(stringCallable);
executorService2.shutdown();
//get操作会造成执行线程的阻塞
String submit2Res = submit2.get();
System.out.println(submit2Res);
System.out.println("main Thread output");
}
}
import java.io.IOException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class TestThreadPoolExecutor {
/*
corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;
当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。
如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;
如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。
值得注意的是,如果使用了无界的任务队列这个参数就没用了
执行:
1、2任务直接压入1、2线程直接创建并执行,
3任务放入缓冲队列,最大线程数未满,创建新线程执行3任务
*/
public static void main(String[] args) throws InterruptedException, IOException {
int corePoolSize = 2;
int maximumPoolSize = 10;
long keepAliveTime = 10;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
ThreadFactory threadFactory = new NameTreadFactory();
RejectedExecutionHandler handler = new MyIgnorePolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit,
workQueue, threadFactory);
executor.prestartAllCoreThreads(); // 预启动所有核心线程
for (int i = 1; i <= 10; i++) {
// Thread.sleep(1);
MyTask task = new MyTask(String.valueOf(i));
executor.execute(task);
System.out.println("当前队列数两"+executor.getQueue().size());
}
System.out.println(executor.getActiveCount());
Thread.sleep(10000);
System.out.println(executor.getActiveCount());
System.in.read(); //阻塞主线程
}
static class NameTreadFactory implements ThreadFactory {
private final AtomicInteger mThreadNum = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
// try {
// Thread.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
Thread t = new Thread(r, "my-thread-" + mThreadNum.getAndIncrement());
System.out.println(t.getName() + " has been created");
return t;
}
}
public static class MyIgnorePolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
doLog(r, e);
}
private void doLog(Runnable r, ThreadPoolExecutor e) {
// 可做日志记录等
System.err.println( r.toString() + " rejected");
// System.out.println("completedTaskCount: " + e.getCompletedTaskCount());
}
}
static class MyTask implements Runnable {
private String name;
public MyTask(String name) {
this.name = name;
}
@Override
public void run() {
try {
System.out.println(this.toString() + " is running!");
Thread.sleep(1000); //让任务执行慢点
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public String getName() {
return name;
}
@Override
public String toString() {
return "MyTask [name=" + name + "]";
}
}
}
public class Syncwaitnotify1 {
public static void main(String[] args) {
final Object o = new Object();
char[] aI = "123456789".toCharArray();
char[] aC = "ABCDEFGHI".toCharArray();
new Thread(()->{
//1. 先拿到o锁
synchronized (o) {
for(char c : aI) {
//2. 执行
System.out.print(c);
try {
o.notify();//唤醒线程队列中得下一个线程, 这里只有两个,就相当于唤醒另一个
o.wait();//让出锁,重新进入等待队列
}catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t1").start();
new Thread(()->{
//1. 和上面得Thread强锁
synchronized (o) {
for(char i : aC) {
System.out.print(i);
try {
o.notify();
o.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t2").start();
}
}
import java.util.concurrent.CountDownLatch;
public class Syncwaitnotify2 {
/*
CountDownLatch latch = new CountDownLatch(n);
latch.countDown();每执行一次,n-1
*/
private static CountDownLatch latch = new CountDownLatch(1);
public static void main(String[] args) {
final Object o = new Object();
char[] aI = "123456789".toCharArray();
char[] aC = "ABCDEFGHI".toCharArray();
new Thread(()->{
try{
latch.await();
}catch (InterruptedException e) {
e.printStackTrace();
}
//1. 先拿到o锁
synchronized (o) {
for(char c : aI) {
//2. 执行
System.out.print(c);
try {
o.notify();//唤醒线程队列中得下一个线程, 这里只有两个,就相当于唤醒另一个
o.wait();//让出锁,重新进入等待队列
}catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t1").start();
new Thread(()->{
//1. 和上面得Thread强锁
synchronized (o) {
for(char i : aC) {
System.out.print(i);
latch.countDown();
try {
o.notify();
o.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t2").start();
}
}
/**
* 如果t1先拿到锁,此时t25 = false 在while中不断得会让出锁
* 等t2拿到锁后, 设置t25=true就可以正常交替执行了
*/
public class Syncwaitnotify3 {
private static volatile boolean t25 = false;
public static void main(String[] args) {
final Object o = new Object();
char[] aI = "123456789".toCharArray();
char[] aC = "ABCDEFGHI".toCharArray();
new Thread(()->{
//1. 如果t1先拿到锁
synchronized (o) {
//2. 此时t25=false
while (!t25) {
try {
//3. 让出线程, 进入等待队列
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for(char c : aI) {
System.out.print(c);
try {
o.notify();//唤醒线程队列中得下一个线程, 这里只有两个,就相当于唤醒另一个
o.wait();//让出锁,重新进入等待队列
}catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t1").start();
new Thread(()->{
//4. 此时t2后拿到锁
synchronized (o) {
for(char i : aC) {
System.out.print(i);
t25 = true;
try {
o.notify();
o.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
o.notify();
}
}, "t2").start();
}
}
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockCondition {
public static void main(String[] args) {
char[] aI = "123456789".toCharArray();
char[] aC = "ABCDEFGHI".toCharArray();
Lock lock = new ReentrantLock();
Condition conditionT1 = lock.newCondition();
Condition conditionT2 = lock.newCondition();
CountDownLatch latch = new CountDownLatch(1);
new Thread(()->{
try {
//1. 启动线程直接进入等待
latch.await();
}catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try {
for(char c : aI) {
System.out.print(c);
conditionT2.signal();
conditionT1.await();
}
conditionT2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
new Thread(()->{
lock.lock();
try {
//2. 线程启动直接进入循环进行输出
for(char c : aC) {
System.out.print(c);
//3. 标记完成
latch.countDown();
//4. 唤醒conditionT1队列
conditionT1.signal();
conditionT2.await();
}
conditionT1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2").start();
}
}
import java.util.concurrent.locks.LockSupport;
public class LockSupport1 {
static Thread t1=null, t2=null;
public static void main(String[] args) {
char[] aI = "123456789".toCharArray();
char[] aC = "ABCDEFGHI".toCharArray();
t1 = new Thread(() -> {
for(char c: aI) {
LockSupport.park();
System.out.print(c);
LockSupport.unpark(t2);
}
},"t1");
t2 = new Thread(() -> {
for(char c: aC) {
System.out.print(c);
LockSupport.unpark(t1);
LockSupport.park();
}
},"t2");
t1.start();
t2.start();
}
}
import java.util.concurrent.LinkedTransferQueue;
public class TransferQueue1 {
public static void main(String[] args) {
char[] aI = "123456789".toCharArray();
char[] aC = "ABCDEFGHI".toCharArray();
java.util.concurrent.TransferQueue<Character> queue = new LinkedTransferQueue<>();
new Thread(() ->{
try{
for(char c: aI) {
System.out.print(queue.take());
queue.transfer(c);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(() ->{
try{
for(char c: aC) {
queue.transfer(c);
System.out.print(queue.take());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
}
}
import java.util.concurrent.*;
/**
* Callable捕获异常
*/
public class CallableGetThrow {
public static void main(String[] args){
int timeout = 2;
ExecutorService executor = Executors.newSingleThreadExecutor();
Boolean result = false;
Future<Boolean> future = executor.submit(new TaskThread("发送请求"));//将任务提交给线程池
try {
System.out.println("try");
result = future.get(timeout, TimeUnit.SECONDS);//获取结果
future.cancel(true);
System.out.println("发送请求任务的返回结果:"+result); //2
} catch (InterruptedException e) {
System.out.println("线程中断出错。"+e);
future.cancel(true);// 中断执行此任务的线程
} catch (ExecutionException e) {
System.out.println("线程服务出错。");
future.cancel(true);
} catch (TimeoutException e) {// 超时异常
System.out.println("超时。");
future.cancel(true);
}finally{
System.out.println("线程服务关闭。");
executor.shutdown();
}
}
static class TaskThread implements Callable<Boolean> {
private String t;
public TaskThread(String temp){
this.t= temp;
}
/*
try
继续执行..........
发送请求任务的返回结果: true
线程服务关闭。
*/
// public Boolean call() throws InterruptedException {
// Thread.currentThread().sleep(1000);
// System.out.println("继续执行..........");
// return true;
// }
/*
try
超时。
线程服务关闭。
*/
// public Boolean call() throws InterruptedException {
// Thread.currentThread().sleep(3000);
// System.out.println("继续执行..........");
// return true;
// }
/*
* try
* start
* 线程服务出错。
* 线程服务关闭。
*/
public Boolean call() throws InterruptedException {
System.out.println("start");
throw new InterruptedException();
}
}
}
{
"wmPlans":
[
{
"skuInfos":
[
{
"customerItemNO":"no-03",
"goCargoId":"go03"
},
{
"customerItemNO":"no-04",
"goCargoId":"go04",
"extensionParameter":
{
"invNo":"abcd01",
"bu":"BM"
}
}
],
"warehouseType":"wmsHouse1",
"warehousedocumentType":"wmsDocument1"
},
{
"skuInfos":
[
{
"customerItemNO":"no-03",
"goCargoId":"go03"
},
{
"customerItemNO":"no-04",
"goCargoId":"go04"
}
],
"warehouseType":"wmsHouse2",
"warehousedocumentType":"wmsDocument2"
}
],
"goOrder":
{
"extionParam":
{
"bbb":"ccc"
},
"skuInfos":
[
{
"customerItemNO":"no-01",
"goCargoId":"go01"
},
{
"customerItemNO":"no-02",
"goCargoId":"go02"
}
],
"i3":20,
"mark3":"mark3",
"consignorCode":"ConsignorCode01"
}
}
我希望能根据一个路径
例如
goOrder.skuInfos.customerItemNO 获取到两个值 no-01 no-02
goOrder.skuInfos[0].customerItemNO 获取到 no-01
wmPlans.skuInfos.customerItemNO 获取到 no-03 no-04 no-03 no-04
wmPlans[0].skuInfos.customerItemNO 获取到 no-03 no-04
wmPlans.skuInfos[0].customerItemNO 获取到 no-03 no-03
不仅仅是获取, 我还希望可以对这个深层次的对象进行赋值 所以有了这个小例子
import lombok.Data;
import lombok.ToString;
import java.util.List;
@Data
@ToString
public class OrderInfoDTO {
private GoOrderDTO goOrder;
private List<WmsPlanDTO> wmPlans;
}
import lombok.Data;
import lombok.ToString;
import java.util.List;
import java.util.Map;
@Data
@ToString
public class GoOrderDTO {
private String consignorCode;
private String mark3;
private Integer i3;
private List<SkuInfoDTO> skuInfos;
private Map<String, String> extionParam;
}
import lombok.Data;
import lombok.ToString;
import java.util.List;
@Data
@ToString
public class WmsPlanDTO {
private String warehouseType;
private String warehousedocumentType;
private List<SkuInfoDTO> skuInfos;
}
import lombok.Data;
import lombok.ToString;
import java.util.Map;
@Data
@ToString
public class SkuInfoDTO {
private String customerItemNO;
private String goCargoId;
private Map<String, String> extensionParameter;
}
import lombok.Data;
@Data
public class ReflectGetPathValRes {
private Boolean success;
private String val;
public ReflectGetPathValRes(Boolean success, String val) {
this.success = success;
this.val = val;
}
}
import cn.hutool.json.JSONUtil;
import org.apache.commons.lang.StringUtils;
import java.lang.reflect.Field;
import java.util.*;
public class ReflectUtil {
/**
* 通过反射,对rootObj 对象 对应conditionPath路径 赋值为val
* @param rootObj
* @param conditionPath
* @param val
* @return
*/
public static boolean reflectAssignEvaluate(Object rootObj, String conditionPath, Object val) {
if (conditionPath.isEmpty()) {
System.out.println("条件比较路径发生空值错误错误");
return false;
}
String[] paths = StringUtils.split(conditionPath, '.');
int pathsLen = paths.length;
Object currObj = rootObj;
Object[] objArr = new Object[pathsLen+1];
objArr[0] = rootObj;
for(int i=0; i <= pathsLen-1; i++) {
try{
Class currObjClass = currObj.getClass();
String currGetDeclaredPath = paths[i];
// 虽然命名是mapKey 但是他也会获取到list的索引值
String mapKey = "";
if (StringUtils.contains(currGetDeclaredPath, '[')) {
currGetDeclaredPath = paths[i].substring(0, paths[i].indexOf('['));
mapKey = paths[i].substring(paths[i].indexOf('[')+1, paths[i].indexOf(']'));
}
Field currObjField = currObjClass.getDeclaredField(currGetDeclaredPath);
currObjField.setAccessible(true);
currObj = currObjField.get(currObj);
if(currObj != null) {
objArr[i+1] = currObj;
}
//如果获取到当前对象为空,并且路径不是最后一级,则为错误
if (currObj == null && i < pathsLen-1) {
System.out.println(currObjField.getType().getTypeName()+"."+currObjField.getName()+"为空,下一级属性无法获取及赋值");
return false;
}
//赋值操作
if(i == pathsLen-1) {
return objAssignEvaluate(currObjField, objArr[i], val, mapKey);
}
if (currObjField.getType().getTypeName().contains("List")) {
List srcList=(List)currObj;
String newPaths = StringUtils.join(arraySub(paths, i+1, pathsLen),'.');
if (StringUtils.isNotBlank(mapKey) && StringUtils.isNumeric(mapKey)) {
int mapKeyIndex = Integer.parseInt(mapKey);
srcList.get(mapKeyIndex);
return reflectAssignEvaluate(srcList.get(mapKeyIndex), newPaths, val);
} else {
boolean assignEvaluateRes = true;
for(Object src : srcList) {
if (!reflectAssignEvaluate(src, newPaths, val)) {
assignEvaluateRes = false;
}
}
return assignEvaluateRes;
}
}
}catch (NoSuchFieldException | IllegalAccessException e) {
System.out.println(e.getMessage());
return false;
}
}
return true;
}
/**
* 利用反射,校验条件路径下对象中的值是否与期望值相同
* 期望值都是String类型,因为是从数据库中获取到的,所以无法判定具体类型
* @param rootObj
* @param conditionPath
* @param expVal
* @return
*/
public static boolean reflectCheckCondition(Object rootObj, String conditionPath, String expVal) {
ReflectGetPathValRes getValRes = reflectGetPathVal(rootObj, conditionPath);
if (getValRes.getSuccess()) {
if (Objects.isNull(getValRes.getVal())) {
getValRes.setVal("");
}
return StringUtils.equals(getValRes.getVal(), expVal);
}
return false;
}
/**
* 通过反射,对rootObj 对象 对应conditionPath路径 代表得值
* 本方法支持map对应得key值
* list获取对应索引后得内容
* @param rootObj
* @param conditionPath
* @return
*/
public static ReflectGetPathValRes reflectGetPathVal(Object rootObj, String conditionPath) {
if (conditionPath.isEmpty()) {
System.out.println("条件比较路径发生空值错误错误");
return new ReflectGetPathValRes(false,"");
}
String[] paths = StringUtils.split(conditionPath, '.');
int pathsLen = paths.length;
Object currObj = rootObj;
Object[] objArr = new Object[pathsLen+1];
objArr[0] = rootObj;
for(int i=0; i <= pathsLen-1; i++) {
try{
Class currObjClass = currObj.getClass();
String currGetDeclaredPath = paths[i];
String mapKey = "";
if (StringUtils.contains(currGetDeclaredPath, '[')) {
currGetDeclaredPath = paths[i].substring(0, paths[i].indexOf('['));
mapKey = paths[i].substring(paths[i].indexOf('[')+1, paths[i].indexOf(']'));
}
Field currObjField = currObjClass.getDeclaredField(currGetDeclaredPath);
currObjField.setAccessible(true);
currObj = currObjField.get(currObj);
if(currObj != null) {
objArr[i+1] = currObj;
}
//如果获取到当前对象为空,并且路径不是最后一级,则为错误
if (currObj == null && i < pathsLen-1) {
System.out.println(currObjField.getType().getTypeName()+"."+currObjField.getName()+"为空,下一级属性无法获取及赋值");
return new ReflectGetPathValRes(false,"");
}
//赋值操作
if(i == pathsLen-1) {
return new ReflectGetPathValRes(true,fromObjGetFieldValue(currObjField, objArr[i], mapKey));
}
if (currObjField.getType().getTypeName().contains("List")) {
List srcList=(List)currObj;
String newPaths = StringUtils.join(arraySub(paths, i+1, pathsLen),'.');
if (StringUtils.isNotBlank(mapKey)) {
return reflectGetPathVal(srcList.get(Integer.parseInt(mapKey)),newPaths);
}
List<Object> resList = new ArrayList<>();
for(Object src : srcList) {
// return reflectGetPathVal(src,newPaths);
ReflectGetPathValRes getPathValRes = reflectGetPathVal(src,newPaths);
// 检查 getPathValRes.getVal() 是否是一个jsonarray
if (getPathValRes.getVal().startsWith("[")) {
resList.addAll(JSONUtil.toList(JSONUtil.parseArray(getPathValRes.getVal()), Object.class));
} else {
resList.add(getPathValRes.getVal());
}
}
return new ReflectGetPathValRes(true, JSONUtil.toJsonStr(resList));
}
}catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
return new ReflectGetPathValRes(false,"");
}
}
return new ReflectGetPathValRes(false,"");
}
public static boolean objAssignEvaluate(Field field, Object rootObj, Object val, String mapKey) throws IllegalAccessException {
if (field.getType().getName().contains("Integer")) {
field.set(rootObj, Integer.parseInt(String.valueOf(val)));return true;
}else if (field.getType().getName().contains("String")) {
field.set(rootObj, String.valueOf(val));
} else if (field.getType().getName().contains("Map")) {
Map<String, String> currMap = (Map<String, String>) field.get(rootObj);
if (currMap == null) {
currMap = new HashMap<>();
}
currMap.put(mapKey, String.valueOf(val));
field.set(rootObj, currMap);
} else if (field.getType().getName().contains("List")) {
List srcList=(List)field.get(rootObj);
if (srcList == null) {
srcList = new ArrayList();
}
if (StringUtils.isNotBlank(mapKey) && StringUtils.isNumeric(mapKey)) {
int index = Integer.valueOf(mapKey);
srcList.set(index, val);
// field.set(rootObj, srcList);
} else {
field.set(rootObj, val);
}
}
//其他类型的强制转换
return true;
}
/**
* 从rootObj中获取字段field得值
* @param field
* @param rootObj
* @param mapKey
* @return
* @throws IllegalAccessException
*/
public static String fromObjGetFieldValue(Field field, Object rootObj, String mapKey) throws IllegalAccessException {
if (field.getType().getName().contains("Integer")) {
Integer realVal = (Integer)field.get(rootObj);
return String.valueOf(realVal);
} else if (field.getType().getName().contains("String")) {
return (String)field.get(rootObj);
} else if (field.getType().getName().contains("Map")) {
Map<String, String> currMap = (Map<String, String>) field.get(rootObj);
return currMap.get(mapKey);
} else if (field.getType().getName().contains("List")) {
List srcList=(List)field.get(rootObj);
// mapkey不是空并且他还是一个数字
if (StringUtils.isNotBlank(mapKey) && StringUtils.isNumeric(mapKey)) {
int index = Integer.valueOf(mapKey);
return JSONUtil.toJsonStr(srcList.get(index));
}
return JSONUtil.toJsonStr(srcList);
}
//其他类型的强制转换
return "";
}
public static String[] arraySub(String[] data, int start, int end) {
String[] c = new String[end-start];
int j = 0;
for(int i=start; i<end; i++){
c[j] = data[i];
j++;
}
return c;
}
//对对象中所有字符串类型的空对象赋值空字符串
public static void javaBeanNullStrToEmptyStr(Object obj){
Class currObjClass = obj.getClass();
Field[] fields = currObjClass.getDeclaredFields();
for(Field field : fields) {
field.setAccessible(true);
if (field.getType().getName().contains("String")) {
try {
String currValue = (String) field.get(obj);
if (StringUtils.isBlank(currValue)) {
objAssignEvaluate(field, obj, "", "");
}
} catch (IllegalAccessException e) {
e.printStackTrace();
System.out.println(e.toString());
}
}
}
}
}
TestMain.java
import cn.hutool.json.JSONUtil;
import java.util.*;
public class TestMain {
public static void main(String[] args) {
OrderInfoDTO orderInfoDTO = initData();
System.out.println(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(orderInfoDTO)));
/********************************************************************************/
// 第一次测试 一个正常的属性路径
// String path1 = "goOrder.consignorCode";
// String expValTrue="ConsignorCode01";
// // 检查一个正确的值
// boolean res11 = ReflectUtil.reflectCheckCondition(orderInfoDTO, path1, expValTrue);//true
// System.out.println(res11); // true
//
// // 重新赋值
// ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path1, "ConsignorCode03");
// System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path1)); // ConsignorCode03
/********************************************************************************/
// 第二次测试 一个arraylist的属性路径
// String path2 = "goOrder.skuInfos";
// String expValTrue="[{\"customerItemNO\":\"no-01\",\"goCargoId\":\"go01\"},{\"customerItemNO\":\"no-02\",\"goCargoId\":\"go02\"}]";
// boolean resTrue = ReflectUtil.reflectCheckCondition(orderInfoDTO, path2, expValTrue);
// System.out.println(resTrue);
// List<SkuInfoDTO> skuList = new ArrayList<>();
// SkuInfoDTO skuInfoDTO1 = new SkuInfoDTO();
// skuInfoDTO1.setGoCargoId("goSku01");
// skuInfoDTO1.setCustomerItemNO("noSKU01");
//
// SkuInfoDTO skuInfoDTO2 = new SkuInfoDTO();
// skuInfoDTO2.setGoCargoId("goSku02");
// skuInfoDTO2.setCustomerItemNO("noSku02");
// skuList.add(skuInfoDTO1);
// skuList.add(skuInfoDTO2);
// ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path2, skuList);
// System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path2));
/********************************************************************************/
// 第三次测试对数组中的某个元素的属性路径
// String path3 = "goOrder.skuInfos[0].goCargoId";
// String expValTrue="go01";
//
// boolean resTrue = ReflectUtil.reflectCheckCondition(orderInfoDTO, path3, expValTrue);
// System.out.println(resTrue); // true
// ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path3, "goCargo01");
// System.out.println(JSONUtil.toJsonStr(orderInfoDTO)); // goCargo01
/********************************************************************************/
// 第四次测试 数组的指定索引进行赋值和取值
// String path4 = "goOrder.skuInfos[0]";
// System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path4));
// String expValTrue="{\"customerItemNO\":\"no-01\",\"goCargoId\":\"go01\"}";
// boolean resTrue = ReflectUtil.reflectCheckCondition(orderInfoDTO, path4, expValTrue);
//
// SkuInfoDTO skuInfoDTO1 = new SkuInfoDTO();
// skuInfoDTO1.setGoCargoId("goSku01");
// skuInfoDTO1.setCustomerItemNO("noSKU01");
// ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path4, skuInfoDTO1);
// System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path4));
/********************************************************************************/
// 第五次测试 此次测试的赋值无意义,几乎不会有吧所有数组元素赋值成完全一样内容的需求
// String path5 = "wmPlans.skuInfos";
// System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path5));
// 这样取值会将上层数组平铺开然后展示出来 [{"customerItemNO":"no-01","goCargoId":"go01"},{"customerItemNO":"no-02","goCargoId":"go02"},{"customerItemNO":"no-03","goCargoId":"go03"},{"customerItemNO":"no-04","goCargoId":"go04","extensionParameter":{"bbb":"ccc"}}]
/********************************************************************************/
// 第六次测试 第一层是数组,第二层指定索引
// String path6 = "wmPlans.skuInfos[0].customerItemNO";
// System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path6));
// ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path6, "shishi");
// System.out.println(JSONUtil.toJsonStr(orderInfoDTO));
/********************************************************************************/
// 第七次测试 第一层是数组指定索引,第二层是数组
// String path7 = "wmPlans[0].skuInfos.customerItemNO";
// System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path7));
// ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path7, "shishi");
// System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path7));
// System.out.println(JSONUtil.toJsonStr(orderInfoDTO));
/********************************************************************************/
// 第八次测试 第一层是数组,然后跟一个普通的属性
// String path8 = "wmPlans.warehouseType";
// System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path8));
// ReflectUtil.reflectAssignEvaluate(orderInfoDTO, path8, "warehouseTypeTh8");
// System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path8));
/********************************************************************************/
// String path9 = "goOrder.skuInfos.goCargoId";
// System.out.println(ReflectUtil.reflectGetPathVal(orderInfoDTO, path9));
// ReflectUtil.reflectAssignEvaluate(orderInfoDTO,path9, "123456");
// System.out.println(JSONUtil.toJsonStr(orderInfoDTO));
/********************************************************************************/
// 第十次测试 给 所有的wmPlans 里面所有的sku 添加扩展属性 aaa = wmsplanPath10keyaaaValue
// String path10 = "wmPlans.skuInfos.extensionParameter[aaa]";
// String val3="wmsplanPath10keyaaaValue";
// ReflectUtil.reflectAssignEvaluate(orderInfoDTO,path10, val3);
// System.out.println(JSONUtil.toJsonStr(orderInfoDTO));
/********************************************************************************/
// 第十一次测试给 wmPlans[0] 的 所有sku 添加扩展属性 aaa = wmsplanPath10keyaaaValue
// String path11 = "wmPlans[0].skuInfos.extensionParameter[aaa]";
// String val11="wmsplanPath11keyaaaValue";
// ReflectUtil.reflectAssignEvaluate(orderInfoDTO,path11, val11);
// System.out.println(JSONUtil.toJsonStr(orderInfoDTO));
/********************************************************************************/
// 第十一次测试 给 所有的wmPlans 的 skuInfos[1] 添加扩展属性 aaa = wmsplanPath12keyaaaValue
// String path12 = "wmPlans.skuInfos[1].extensionParameter[aaa]";
// String val12="wmsplanPath12keyaaaValue";
// ReflectUtil.reflectAssignEvaluate(orderInfoDTO,path12, val12);
// System.out.println(JSONUtil.toJsonStr(orderInfoDTO));
/********************************************************************************/
}
public static OrderInfoDTO initData() {
SkuInfoDTO skuInfoDTO1 = new SkuInfoDTO();
skuInfoDTO1.setGoCargoId("go01");
skuInfoDTO1.setCustomerItemNO("no-01");
SkuInfoDTO skuInfoDTO2 = new SkuInfoDTO();
skuInfoDTO2.setGoCargoId("go02");
skuInfoDTO2.setCustomerItemNO("no-02");
Map<String, String> goOrderEXTMap1 = new HashMap<>();
goOrderEXTMap1.put("bbb", "ccc");
List<SkuInfoDTO> goOrderSKUInfos = new ArrayList<>();
goOrderSKUInfos.add(skuInfoDTO1);
goOrderSKUInfos.add(skuInfoDTO2);
GoOrderDTO goOrderDTO = new GoOrderDTO();
goOrderDTO.setConsignorCode("ConsignorCode01");
goOrderDTO.setMark3("mark3");
goOrderDTO.setI3(20);
goOrderDTO.setSkuInfos(goOrderSKUInfos);
goOrderDTO.setExtionParam(goOrderEXTMap1);
// goOrder赋值完成
SkuInfoDTO wmsSku0101 = new SkuInfoDTO();
wmsSku0101.setGoCargoId("go03");
wmsSku0101.setCustomerItemNO("no-03");
SkuInfoDTO wmsSku0102 = new SkuInfoDTO();
wmsSku0102.setGoCargoId("go04");
wmsSku0102.setCustomerItemNO("no-04");
Map<String, String> wmsSku0102ExtionParam = new HashMap<>();
wmsSku0102ExtionParam.put("invNo", "abcd01");
wmsSku0102ExtionParam.put("bu", "BM");
wmsSku0102.setExtensionParameter(wmsSku0102ExtionParam);
WmsPlanDTO wmsPlanDTO1 = new WmsPlanDTO();
wmsPlanDTO1.setWarehouseType("wmsHouse1");
wmsPlanDTO1.setWarehousedocumentType("wmsDocument1");
wmsPlanDTO1.setSkuInfos(Arrays.asList(wmsSku0101, wmsSku0102));
// wmsPlanDTO1赋值完成
SkuInfoDTO wmsSku0201 = new SkuInfoDTO();
wmsSku0201.setGoCargoId("go03");
wmsSku0201.setCustomerItemNO("no-03");
SkuInfoDTO wmsSku0202 = new SkuInfoDTO();
wmsSku0202.setGoCargoId("go04");
wmsSku0202.setCustomerItemNO("no-04");
Map<String, String> wmsSku0202ExtionParam = new HashMap<>();
wmsSku0202ExtionParam.put("invNo", "abcd01");
wmsSku0202ExtionParam.put("bu", "BM");
wmsSku0102.setExtensionParameter(wmsSku0202ExtionParam);
WmsPlanDTO wmsPlanDTO2 = new WmsPlanDTO();
wmsPlanDTO2.setWarehouseType("wmsHouse2");
wmsPlanDTO2.setWarehousedocumentType("wmsDocument2");
wmsPlanDTO2.setSkuInfos(Arrays.asList(wmsSku0201, wmsSku0202));
// wmsPlanDTO2赋值完成
OrderInfoDTO orderInfoDTO = new OrderInfoDTO();
orderInfoDTO.setGoOrder(goOrderDTO);
orderInfoDTO.setWmPlans(Arrays.asList(wmsPlanDTO1, wmsPlanDTO2));
return orderInfoDTO;
}
}
package cn.anzhongwei.lean.demo.bigdecimal;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 用于高精确处理常用的数学运算
*/
public class ArithmeticUtils {
public static void main(String[] args) {
System.out.println("2.5 to "+toCeiling(new BigDecimal(2.5)));
System.out.println("4 to "+toCeiling(new BigDecimal(4)));
System.out.println("4.023 to "+toCeiling(new BigDecimal(4.023)));
System.out.println("0.5 to "+toCeiling(new BigDecimal(0.5)));
System.out.println("-0.5 to "+toCeiling(new BigDecimal(-0.5)));
System.out.println("-1.5 to "+toCeiling(new BigDecimal(-1.5)));
System.out.println(new BigDecimal("1.000").abs());
System.out.println(new BigDecimal("1").abs());
BigDecimal b1 = new BigDecimal("5");
BigDecimal b2 = new BigDecimal("4.000");
System.out.println(b1.compareTo(b2));
}
//默认除法运算精度
private static final int DEF_DIV_SCALE = 10;
/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static double add(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.add(b2).doubleValue();
}
/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static BigDecimal add(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.add(b2);
}
/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @param scale 保留scale 位小数
* @return 两个参数的和
*/
public static String add(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.add(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 提供精确的减法运算
*
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static double sub(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.subtract(b2).doubleValue();
}
/**
* 提供精确的减法运算。
*
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static BigDecimal sub(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.subtract(b2);
}
/**
* 提供精确的减法运算
*
* @param v1 被减数
* @param v2 减数
* @param scale 保留scale 位小数
* @return 两个参数的差
*/
public static String sub(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.subtract(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static double mul(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.multiply(b2).doubleValue();
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static BigDecimal mul(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.multiply(b2);
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @param scale 保留scale 位小数
* @return 两个参数的积
*/
public static double mul(double v1, double v2, int scale) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return round(b1.multiply(b2).doubleValue(), scale);
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @param scale 保留scale 位小数
* @return 两个参数的积
*/
public static String mul(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.multiply(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
* 小数点以后10位,以后的数字四舍五入
*
* @param v1 被除数
* @param v2 除数
* @return 两个参数的商
*/
public static double div(double v1, double v2) {
return div(v1, v2, DEF_DIV_SCALE);
}
/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示表示需要精确到小数点以后几位。
* @return 两个参数的商
*/
public static double div(double v1, double v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示需要精确到小数点以后几位
* @return 两个参数的商
*/
public static String div(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v1);
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 提供精确的小数位四舍五入处理
*
* @param v 需要四舍五入的数字
* @param scale 小数点后保留几位
* @return 四舍五入后的结果
*/
public static double round(double v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b = new BigDecimal(Double.toString(v));
return b.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
* 提供精确的小数位四舍五入处理
*
* @param v 需要四舍五入的数字
* @param scale 小数点后保留几位
* @return 四舍五入后的结果
*/
public static String round(String v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b = new BigDecimal(v);
return b.setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 取余数
*
* @param v1 被除数
* @param v2 除数
* @param scale 小数点后保留几位
* @return 余数
*/
public static String remainder(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.remainder(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
}
/**
* 取余数 BigDecimal
*
* @param v1 被除数
* @param v2 除数
* @param scale 小数点后保留几位
* @return 余数
*/
public static BigDecimal remainder(BigDecimal v1, BigDecimal v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
return v1.remainder(v2).setScale(scale, BigDecimal.ROUND_HALF_UP);
}
/**
* 比较大小
*
* @param v1 被比较数
* @param v2 比较数
* @return 如果v1 大于v2 则 返回true 否则false
*/
public static boolean compare(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
int bj = b1.compareTo(b2);
boolean res;
if (bj > 0)
res = true;
else
res = false;
return res;
}
public static BigDecimal toCeiling(BigDecimal val){
// if (val.compareTo(new BigDecimal(1)) >= 0) {
// return val.round(new MathContext(1, RoundingMode.CEILING));
// } else {
return val.setScale(0, RoundingMode.CEILING);
// }
}
}
运行结果
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
@AllArgsConstructor
@Data
@ToString
class Car implements Serializable {
private static final long serialVersionUID = -5713945027627603702L;
private String brand; // 品牌
private int maxSpeed; // 最高时速
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
@AllArgsConstructor
@Data
@ToString
class Person implements Serializable {
private static final long serialVersionUID = -9102017020286042305L;
private String name; // 姓名
private int age; // 年龄
private Car car; // 座驾
}
import java.io.*;
/**
* 浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝(例:assign())
*
* 深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝(例:JSON.parse()和JSON.stringify(),但是此方法无法复制函数类型)
*/
public class CloneUtil {
public static void main(String[] args) {
try {
Person p1 = new Person("郭靖", 33, new Car("Benz", 300));
Person p2 = CloneUtil.clone(p1); // 深度克隆
p2.getCar().setBrand("BYD");
// 修改克隆的Person对象p2关联的汽车对象的品牌属性
// 原来的Person对象p1关联的汽车不会受到任何影响
// 因为在克隆Person对象时其关联的汽车对象也被克隆了
System.out.println(p1);
} catch (Exception e) {
e.printStackTrace();
}
}
// 私有构造方法
private CloneUtil() {
throw new AssertionError();
}
/*
注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,
可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,
这种是方案明显优于使用Object类的clone方法克隆对象。
让问题在编译的时候暴露出来总是好过把问题留到运行时。
*/
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj) throws Exception {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bout);
oos.writeObject(obj);
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bin);
return (T) ois.readObject();
// 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义
// 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放
}
}
package cn.anzhongwei.lean.demo.date;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;
public class DateUtils {
private static final DateTimeFormatter YEAH_MONTH_DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter YEAH_MONTH_DAY_HOUR_MINUTE_SECOND = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public DateUtils() {
}
public static void main(String[] args) {
System.out.println("1. 获取当前日期字符串: " + getNowDateTime());
//
System.out.println("2.1 获取UTC时间: " + getUTCTime());
System.out.println("2.2 获取时间戳: " + getTimestamp());
//
System.out.println("3. 根据字符串日期转LocalDate: " + getString2LocalDate("2019-01-01"));
System.out.println("4. 根据字符串日期时间转 LocalDateTime: " + getString2FullDate("2019-01-01 00:00:00"));
System.out.println("5. 根据自定义格式获取当前日期: " + getNowDateByFormatter("yyyy++MM++dd"));
System.out.println("6. 根据自定义格式获取当前日期时间: " + getNowDateTimeByFormatter("yyyy++MM++dd HH:mm:ss"));
System.out.printf(
"""
7. 根据节点获取当前日期时间信息
%s 年 %s 月 %s 日 星期 %s 是今年的第%s天
%s时 %s分 %s秒
%n""", getNowDateNode(DateEnum.YEAR),
getNowDateNode(DateEnum.MONTH),
getNowDateNode(DateEnum.DAY),
getNowDateNode(DateEnum.WEEK),
getNowDateNode(DateEnum.DAYOFYEAR),
getNowDateNode(DateEnum.HOUR),
getNowDateNode(DateEnum.MINUTE),
getNowDateNode(DateEnum.SECOND)
);
System.out.println("8. 一年后的日期时间: " + getPreOrAfterDateTime(DateEnum.YEAR, 1));
System.out.println("8. 一年前的日期时间: " + getPreOrAfterDateTime(DateEnum.YEAR, -1));
System.out.println("8. 一小时后的日期时间: " + getPreOrAfterDateTime(DateEnum.HOUR, 1));
System.out.println("8. 一小时前的日期时间: " + getPreOrAfterDateTime(DateEnum.HOUR, -1));
System.out.println("9. 比较两个日期时间大小 2019-01-01 00:00:00 & 2019-01-01 00:00:01: " + compareDateTime("2019-01-01 00:00:00", "2019-01-01 00:00:01"));
System.out.println("10. 判断是否闰年: " + isLeapYear("2024-01-01"));
System.out.println("11. 两个日期间隔天数: " + getPeridNum("2019-01-01", "2019-01-01", DateEnum.DAY));
System.out.println("12. 获取当前时区:" + getTimeZone());
System.out.println("13. 获取America/Los_Angeles时区时间 ZoneId.SHORT_IDS:" + getTimeZoneDateTime("America/Los_Angeles"));
}
// 1. 获取当前日期时间字符串
// 改成LocalDate.now().toString() 获取当前日期字符串
// 改成LocalTime.now().toString() 获取当前时间字符串
public static String getNowDateTime() {
return LocalDateTime.now().format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
}
// 2.1 获得utd时间
public static String getUTCTime() {
//Clock类
return Clock.systemUTC().instant().toString();
}
// 2.2 获得时间戳
public static Long getTimestamp() {
//Clock类
return Clock.systemUTC().millis();
}
// 3. 根据字符串日期转LocalDate
public static LocalDate getString2LocalDate(String date) {
return LocalDate.parse(date, YEAH_MONTH_DAY);
}
// 4. 根据字符串日期时间转 LocalDateTime
public static LocalDateTime getString2FullDate(String date) {
return LocalDateTime.parse(date, YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
}
// 5. 根据自定义格式获取当前日期
public static String getNowDateByFormatter(String formatterPattern) {
return LocalDate.now().format(DateTimeFormatter.ofPattern(formatterPattern));
}
// 6. 根据自定义格式获取当前日期时间
public static String getNowDateTimeByFormatter(String formatterPattern) {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(formatterPattern));
}
// 7. 获取当前 节点
public static Integer getNowDateNode(DateEnum dateEnum) {
LocalDateTime nowDate = LocalDateTime.now();
Integer nowNode = null;
switch (dateEnum) {
case YEAR:
nowNode = nowDate.getYear();
break;
case MONTH:
nowNode = nowDate.getMonthValue();
break;
case WEEK:
nowNode = conversionWeek2Num(DateUtils.WeekEnum.valueOf(nowDate.getDayOfWeek().toString()));
break;
case DAY:
nowNode = nowDate.getDayOfMonth();
break;
case DAYOFYEAR:
nowNode = nowDate.getDayOfYear();
break;
case HOUR:
nowNode = nowDate.getHour();
break;
case MINUTE:
nowNode = nowDate.getMinute();
break;
case SECOND:
nowNode = nowDate.getSecond();
}
return nowNode;
}
// 8. 从当前日期加或减一个 年 月 日 周 时 分 秒 的时间 加就是正数 减就是负数
// LocalDateTime nowDate = LocalDateTime.now();
// 改成 LocalDate nowDate = LocalDate.now(); 就是只能获取 年月日周
// 改成 LocalTime nowDate = LocalTime.now(); 就是只能获取 时 分 秒
public static String getPreOrAfterDateTime(DateEnum dateEnum, long time) {
LocalDateTime nowDate = LocalDateTime.now();
switch (dateEnum) {
case YEAR:
// 从api 的角度也可以用 这个api提供的时间单位更多
// nowDate.plus(time, ChronoUnit.YEARS);
// nowDate.minus(time, ChronoUnit.YEARS)
return nowDate.plusYears(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
case MONTH:
return nowDate.plusMonths(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
case WEEK:
return nowDate.plusWeeks(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
case DAY:
return nowDate.plusDays(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
case DAYOFYEAR:
throw new RuntimeException("不支持的选项");
case HOUR:
return nowDate.plusHours(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
case MINUTE:
return nowDate.plusMinutes(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
case SECOND:
return nowDate.plusSeconds(time).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
default:
throw new RuntimeException("不支持的选项");
}
}
// 9. 比较日期时间 begin 比 end 早 返回true
// 同理LocalDate比较日期 LocalTime 比较时间
public static boolean compareDateTime(String begin, String end) {
LocalDateTime beginDate = LocalDateTime.parse(begin, YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
LocalDateTime endDate = LocalDateTime.parse(end, YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
return beginDate.isBefore(endDate);
}
// 10. 判断是否闰年
public static boolean isLeapYear(String date) {
return LocalDate.parse(date.trim()).isLeapYear();
}
// 11. 获取两个日期之间的间隔 这个函数只能用LocalDate
public static Integer getPeridNum(String begin, String end, DateEnum dateEnum) {
switch (dateEnum) {
case YEAR:
return Period.between(LocalDate.parse(begin), LocalDate.parse(end)).getYears();
case MONTH:
return Period.between(LocalDate.parse(begin), LocalDate.parse(end)).getMonths();
case WEEK:
throw new RuntimeException("不支持的参数");
case DAY:
return Period.between(LocalDate.parse(begin), LocalDate.parse(end)).getDays();
default:
throw new RuntimeException("不支持的参数");
}
}
// 12. 获取当前时区
public static String getTimeZone() {
return ZoneId.systemDefault().toString();
}
// 13. 获取指定时区时间 ZoneId.SHORT_IDS
public static String getTimeZoneDateTime(String timeZone) {
return LocalDateTime.now(ZoneId.of(timeZone)).format(YEAH_MONTH_DAY_HOUR_MINUTE_SECOND);
}
public static int conversionWeek2Num(WeekEnum weekEnum) {
switch (weekEnum) {
case MONDAY:
return 1;
case TUESDAY:
return 2;
case WEDNESDAY:
return 3;
case THURSDAY:
return 4;
case FRIDAY:
return 5;
case SATURDAY:
return 6;
case SUNDAY:
return 7;
default:
return -1;
}
}
public static enum WeekEnum {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY;
private WeekEnum() {
}
}
public static enum DateEnum {
YEAR,
MONTH,
WEEK,
DAY,
HOUR,
MINUTE,
SECOND,
DAYOFYEAR;
private DateEnum() {
}
}
}
package cn.anzhongwei.lean.demo.abstractImplementInterface;
import java.math.BigDecimal;
/**
* 接口中定义3个方法
*/
public interface InterfaceA {
int a();
String b();
BigDecimal c();
}
package cn.anzhongwei.lean.demo.abstractImplementInterface;
/**
* 抽象中去实现两个方法,只有c()没有在抽象类中实现,留给最后类去实现c
*
*/
public abstract class AbstractB implements InterfaceA{
@Override
public int a() {
return 1;
}
@Override
public String b() {
return "Hello World!";
}
}
package cn.anzhongwei.lean.demo.abstractImplementInterface;
import java.math.BigDecimal;
/**
* 由类来实现抽象中未实现得方法, 这样得好处是可以将抽象当成通用得实现,每个最终得实现只去实现差异即可,
* 既有通用得又有灵活的方法
*/
public class ClassC extends AbstractB{
@Override
public BigDecimal c() {
return new BigDecimal(20);
}
}
package cn.anzhongwei.lean.demo.abstractImplementInterface;
public class TestMain {
public static void main(String[] args) {
InterfaceA interfaceA1 = new ClassC();
System.out.println(interfaceA1.a()); // 1
System.out.println(interfaceA1.b()); // Hello World!
System.out.println(interfaceA1.c()); // 20
AbstractB abstractB2 = new ClassC();
System.out.println(abstractB2.a()); // 1
System.out.println(abstractB2.b()); // Hello World!
System.out.println(abstractB2.c()); // 20
ClassC classc3 = new ClassC();
System.out.println(classc3.a()); // 1
System.out.println(classc3.b()); // Hello World!
System.out.println(classc3.c()); // 20
}
}
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
public class NIOFileReadAndWrite {
private String file;
public String getFile() {
return file;
}
public void setFile(String file) {
this.file = file;
}
public NIOFileReadAndWrite(String file) throws IOException {
super();
this.file = file;
}
/**
* NIO读取文件
* @param allocate
* @throws IOException
*/
public void read(int allocate) throws IOException {
RandomAccessFile access = new RandomAccessFile(this.file, "r");
//FileInputStream inputStream = new FileInputStream(this.file);
FileChannel channel = access.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(allocate);
CharBuffer charBuffer = CharBuffer.allocate(allocate);
Charset charset = Charset.forName("GBK");
CharsetDecoder decoder = charset.newDecoder();
int length = channel.read(byteBuffer);
while (length != -1) {
byteBuffer.flip();
decoder.decode(byteBuffer, charBuffer, true);
charBuffer.flip();
System.out.println(charBuffer.toString());
// 清空缓存
byteBuffer.clear();
charBuffer.clear();
// 再次读取文本内容
length = channel.read(byteBuffer);
}
channel.close();
if (access != null) {
access.close();
}
}
/**
* NIO写文件
* @param context
* @param allocate
* @param chartName
* @throws IOException
*/
public void write(String context, int allocate, String chartName) throws IOException{
// FileOutputStream outputStream = new FileOutputStream(this.file); //文件内容覆盖模式 --不推荐
FileOutputStream outputStream = new FileOutputStream(this.file, true); //文件内容追加模式--推荐
FileChannel channel = outputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(allocate);
byteBuffer.put(context.getBytes(chartName));
byteBuffer.flip();//读取模式转换为写入模式
channel.write(byteBuffer);
channel.close();
if(outputStream != null){
outputStream.close();
}
}
/**
* nio事实现文件拷贝
* @param source
* @param target
* @param allocate
* @throws IOException
*/
public static void nioCpoy(String source, String target, int allocate) throws IOException{
ByteBuffer byteBuffer = ByteBuffer.allocate(allocate);
FileInputStream inputStream = new FileInputStream(source);
FileChannel inChannel = inputStream.getChannel();
FileOutputStream outputStream = new FileOutputStream(target);
FileChannel outChannel = outputStream.getChannel();
int length = inChannel.read(byteBuffer);
while(length != -1){
byteBuffer.flip();//读取模式转换写入模式
outChannel.write(byteBuffer);
byteBuffer.clear(); //清空缓存,等待下次写入
// 再次读取文本内容
length = inChannel.read(byteBuffer);
}
outputStream.close();
outChannel.close();
inputStream.close();
inChannel.close();
}
public static void fileChannelCopy(String sfPath, String tfPath) {
File sf = new File(sfPath);
File tf = new File(tfPath);
FileInputStream fi = null;
FileOutputStream fo = null;
FileChannel in = null;
FileChannel out = null;
try{
fi = new FileInputStream(sf);
fo = new FileOutputStream(tf);
in = fi.getChannel();//得到对应的文件通道
out = fo.getChannel();//得到对应的文件通道
in.transferTo(0, in.size(), out);//连接两个通道,并且从in通道读取,然后写入out通道
}catch (Exception e){
e.printStackTrace();
}finally {
try{
fi.close();
in.close();
fo.close();
out.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
//IO方法实现文件k拷贝
private static void traditionalCopy(String sourcePath, String destPath) throws Exception {
File source = new File(sourcePath);
File dest = new File(destPath);
if (!dest.exists()) {
dest.createNewFile();
}
FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(dest);
byte[] buf = new byte[1024];
int len = 0;
while ((len = fis.read(buf)) != -1) {
fos.write(buf, 0, len);
}
fis.close();
fos.close();
}
public static void main(String[] args) throws Exception{
long start = System.currentTimeMillis();
nioCpoy("D:\\迅雷下载\\jdk-21_windows-x64_bin.zip", "D:\\qwe\\jdk-21_windows-x64_bin.zip",10240);
long end = System.currentTimeMillis();
System.out.println("用时为:" + (end-start));
long start2 = System.currentTimeMillis();
fileChannelCopy("D:\\迅雷下载\\jdk-21_windows-x64_bin.zip", "D:\\qwe\\jdk-21_windows-x64_bin.zip");
long end2 = System.currentTimeMillis();
System.out.println("用时为:" + (end2-start2));
long start3 = System.currentTimeMillis();
traditionalCopy("D:\\迅雷下载\\jdk-21_windows-x64_bin.zip", "D:\\qwe\\jdk-21_windows-x64_bin.zip");
long end3 = System.currentTimeMillis();
System.out.println("用时为:" + (end3-start3));
}
}
package cn.anzhongwei.lean.demo.downfileforweb;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
@RestController
@ResponseBody
public class DownImgController {
//http://localhost:8080/ttpng?seriesUniqueCode=1
@GetMapping(value = "/writeImg1")
public void writeImg1(HttpServletResponse response) throws IOException {
//设置响应头信息需要在输出流之前, 就可以触发浏览器下载机制, 注释下列3行或将其写在输出流之后, 都只会在浏览器显示不会触发下载
// response.setHeader("Content-disposition", "attachment;filename=cargoExport.jpg");
// response.setContentType("multipart/form-data");
// response.setCharacterEncoding(StandardCharsets.UTF_8.name());
URL url = null;
InputStream is = null;
ByteArrayOutputStream outStream = null;
HttpURLConnection httpUrl = null;
//随便找一张图片
url = new URL("http://img.ay1.cc/uploads/20221114/9f3e884272b562cf8b27a048c3634621.jpg");
httpUrl = (HttpURLConnection) url.openConnection();
httpUrl.connect();
httpUrl.getInputStream();
is = httpUrl.getInputStream();
outStream = new ByteArrayOutputStream();
//创建一个Buffer字符串
byte[] buffer = new byte[1024];
//每次读取的字符串长度,如果为-1,代表全部读取完毕
int len = 0;
//使用一个输入流从buffer里把数据读取出来
while ((len = is.read(buffer)) != -1) {
//用输出流往buffer里写入数据,中间参数代表从哪个位置开始读,len代表读取的长度
response.getOutputStream().write(buffer, 0, len);
}
response.flushBuffer();
}
@GetMapping(value = "/writeImg2", produces = MediaType.IMAGE_JPEG_VALUE)
public void writeImg2(HttpServletResponse response) throws IOException {
URL url = null;
InputStream is = null;
ByteArrayOutputStream outStream = null;
HttpURLConnection httpUrl = null;
url = new URL("http://img.ay1.cc/uploads/20221114/9f3e884272b562cf8b27a048c3634621.jpg");
httpUrl = (HttpURLConnection) url.openConnection();
httpUrl.connect();
httpUrl.getInputStream();
is = httpUrl.getInputStream();
outStream = new ByteArrayOutputStream();
//创建一个Buffer字符串
byte[] buffer = new byte[1024];
//每次读取的字符串长度,如果为-1,代表全部读取完毕
int len = 0;
//使用一个输入流从buffer里把数据读取出来
while ((len = is.read(buffer)) != -1) {
//用输出流往buffer里写入数据,中间参数代表从哪个位置开始读,len代表读取的长度
outStream.write(buffer, 0, len);
}
byte[] temp = outStream.toByteArray();
response.setHeader("Content-disposition", "attachment;filename=cargoExport.jpg");
response.setContentType("multipart/form-data");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getOutputStream().write(temp);
response.flushBuffer();
}
public void imgToBase64() {
URL downUrl = new URL("http://localhost/test.png");
HttpURLConnection conn = (HttpURLConnection) downUrl.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
InputStream inputStream = conn.getInputStream();
ByteArrayOutputStream data = new ByteArrayOutputStream();
byte[] by = new byte[1024];
int len = -1;
while((len = inputStream.read(by)) != -1){
data.write(by,0,len);
}
inputStream.close();
Base64.getEncoder().encode(data.toByteArray());
String imgStr = Base64.getEncoder().encodeToString(data.toByteArray());
System.out.println(imgStr);
}
}
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class JavaPostJson2 {
final static String url = "http://localhost:1111/sendData";
// final static String params = "{\"token\":\"12345\","
// + "\"appId\":\"20180628173051169c85d965d164ed9ab1281d22ff350ec19\""
// + "\"appName\":\"test33\""
// + "\"classTopic\":\"112\""
// + "\"eventTag\":\"22\""
// + "\"msg\":{}"
// + "}";
final static String params = "{\"token\":\"12345\","
+ "\"appId\":\"20180628173051169c85d965d164ed9ab1281d22ff350ec19\","
+ "\"appName\":\"test33\","
+ "\"classTopic\":\"112\","
+ "\"eventTag\":\"22\""
+ "}";
public static void main(String[] args) {
String res = post(url, params);
System.out.println(res);
}
/**
* 发送HttpPost请求
*
* @param strURL
* 服务地址
* @param params
* json字符串,例如: "{ \"id\":\"12345\" }" ;其中属性名必须带双引号<br/>
* @return 成功:返回json字符串<br/>
*/
public static String post(String strURL, String params) {
System.out.println(strURL);
System.out.println(params);
BufferedReader reader = null;
try {
URL url = new URL(strURL);// 创建连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setUseCaches(false);
connection.setInstanceFollowRedirects(true);
connection.setRequestMethod("POST"); // 设置请求方式
// connection.setRequestProperty("Accept", "application/json"); // 设置接收数据的格式
connection.setRequestProperty("Content-Type", "application/json"); // 设置发送数据的格式
connection.connect();
OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream(), "UTF-8"); // utf-8编码
out.append(params);
out.flush();
out.close();
// 读取响应
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
String line;
String res = "";
while ((line = reader.readLine()) != null) {
res += line;
}
reader.close();
return res;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return "error"; // 自定义错误信息
}
}
要求1: 在客户端连接后打印客户端端的ip和端口号
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class DemoNetty101 {
public static void main(String[] args) {
DemoNetty101 demoNetty101 = new DemoNetty101();
demoNetty101.startTCP();
}
private void startTCP() {
// 一个监听工作组,在很多写法中叫做master组
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 一个工作监听组,在很多写法中交child组
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup)
// 设置非阻塞模式
.channel(NioServerSocketChannel.class)
// 添加自定义处理器
.childHandler(new TCPServerInitializer());
ChannelFuture f = server.bind(8088).sync();
System.out.println("Http Server started, Listening on " + 8088);
f.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
public class TCPServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
System.out.println("初始化连接成功");
System.out.println(socketChannel.remoteAddress().getHostString()+":"+socketChannel.remoteAddress().getPort());
}
}
要求1: 在客户端连接后打印客户端端的ip和端口号 要求2:能打印出客户端发送的内容
要求1: 在客户端连接后打印客户端端的ip和端口号 要求2: 使用内置的协议解析器解码客户端发送的内容 要求3:使用字符串解码器解码, 打印出客户端发送的内容
要求1: 让入站数据经过多个Handle处理器 要求2: 每个处理器都知道客户端关闭了连接
要求1: 能将收到的内容原样回复给客户端程序(这里的变化主要是要将返回的数据进行编码)
要求1: 使用内置的字符串编解码器
要求1: 接收多个客户端链接(其实就是可以缓存多个客户端链接),并每次响应给所有客户端(群发消息)
要求1: 新建客户端能正常连接到一个服务端
要求1: 新建客户端能正常连接到一个服务端 要求2: 使用netty自带的换行解析器拆分数据, 并使用字符串解码编码器来打印服务端信息和返回服务端字符串
要求1: 使用继承ChannelInboundHandlerAdapter处理入站信息 使用继承ChannelOutboundHandlerAdapter处理出站时间
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
System.out.println("链接报告开始");
System.out.println("链接报告信息:本客户端链接到服务端。channelId:" + socketChannel.id());
System.out.println("链接报告完毕");
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024))
.addLast(new StringDecoder(Charset.forName("GBK")))
.addLast(new StringEncoder(Charset.forName("GBK")))
.addLast(new Outbound1())
.addLast(new Outbound2())
.addLast(new Inbound1())
//必须使用ctx.writeAndFlush(message); 否则出站处理器收不到消息,
//如果定义到Outbound前面相当于数据提前掉头了
.addLast(new Inbound2())
;
}
//在Inbound2中必须使用ctx.writeAndFlush(message); 也就是最后一个入站处理器必须使用ctx.writeAndFlush(message); 否则出站处理器收不到消息
// 同时如果在Inbound1中使用了ctx.writeAndFlush(message); 那么数据将不再向Inbound2中传递
//根据处理器执行顺序,数据会先走Outbound2 再走Outbound1, 所以再Outbund1中必须调用ctx.writeAndFlush(message, promise);
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;
/**
* 可以实现在普通类中获取springBean实例
*/
@Component
public final class GetSpringBeanUtil implements BeanFactoryPostProcessor {
private static ConfigurableListableBeanFactory beanFactory;
public GetSpringBeanUtil() {
}
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
GetSpringBeanUtil.beanFactory = beanFactory;
}
public static <T> T getBean(String name) throws BeansException {
return (T) beanFactory.getBean(name);
}
public static <T> T getBean(Class<T> clz) throws BeansException {
T result = beanFactory.getBean(clz);
return result;
}
public static boolean containsBean(String name) {
return beanFactory.containsBean(name);
}
public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
return beanFactory.isSingleton(name);
}
public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
return beanFactory.getType(name);
}
public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
return beanFactory.getAliases(name);
}
public static <T> T getAopProxy(T invoker) {
return (T) AopContext.currentProxy();
}
}
本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
ClassLoader翻译过来就是类加载器,普通的java开发者其实用到的不多,但对于某些框架开发者来说却非常常见。理解ClassLoader的加载机制,也有利于我们编写出更高效的代码。ClassLoader的具体作用就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了。但是,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。想想也是的,一次性加载那么多jar包那么多class,那内存不崩溃。本文的目的也是学习ClassLoader这种加载机制。
备注:本文篇幅比较长,但内容简单,大家不要恐慌,安静地耐心翻阅就是
我们都知道在Java中程序是运行在虚拟机中,我们平常用文本编辑器或者是IDE编写的程序都是.java格式的文件,这是最基础的源码,但这类文件是不能直接运行的。如我们编写一个简单的程序HelloWorld.java
public class HelloWorld{
public static void main(String[] args){
System.out.println("Hello world!");
}
}
如图:

然后,我们需要在命令行中进行java文件的编译
javac HelloWorld.java

可以看到目录下生成了.class文件
我们再从命令行中执行命令:
java HelloWorld

上面是基本代码示例,是所有入门JAVA语言时都学过的东西,这里重新拿出来是想让大家将焦点回到class文件上,class文件是字节码格式文件,java虚拟机并不能直接识别我们平常编写的.java源文件,所以需要javac这个命令转换成.class文件。另外,如果用C或者PYTHON编写的程序正确转换成.class文件后,java虚拟机也是可以识别运行的。更多信息大家可以参考这篇。
了解了.class文件后,我们再来思考下,我们平常在Eclipse中编写的java程序是如何运行的,也就是我们自己编写的各种类是如何被加载到jvm(java虚拟机)中去的。
初学java的时候,最害怕的就是下载JDK后要配置环境变量了,关键是当时不理解,所以战战兢兢地照着书籍上或者是网络上的介绍进行操作。然后下次再弄的时候,又忘记了而且是必忘。当时,心里的想法很气愤的,想着是–这东西一点也不人性化,为什么非要自己配置环境变量呢?太不照顾菜鸟和新手了,很多菜鸟就是因为卡在环境变量的配置上,遭受了太多的挫败感。
因为我是在Windows下编程的,所以只讲Window平台上的环境变量,主要有3个:JAVA_HOME、PATH、CLASSPATH。
指的是你JDK安装的位置,一般默认安装在C盘,如
C:\Program Files\Java\jdk1.8.0_91
将程序路径包含在PATH当中后,在命令行窗口就可以直接键入它的名字了,而不再需要键入它的全路径,比如上面代码中我用的到javac和java两个命令。
一般的
PATH=%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;%PATH%;
也就是在原来的PATH路径上添加JDK目录下的bin目录和jre目录的bin.
CLASSPATH=.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar
一看就是指向jar包路径。
需要注意的是前面的.;,.代表当前目录。
设置可以右击我的电脑,然后点击属性,再点击高级,然后点击环境变量,具体不明白的自行查阅文档。
查看的话可以打开命令行窗口
echo %JAVA_HOME%
echo %PATH%
echo %CLASSPATH%
好了,扯远了,知道了环境变量,特别是CLASSPATH时,我们进入今天的主题Classloader.
Java语言系统自带有三个类加载器:
java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。-D java.ext.dirs选项指定的目录。我们上面简单介绍了3个ClassLoader。说明了它们加载的路径。并且还提到了-Xbootclasspath和-D java.ext.dirs这两个虚拟机参数选项。
我们看到了系统的3个类加载器,但我们可能不知道具体哪个先行呢?
我可以先告诉你答案
为了更好的理解,我们可以查看源码。
看sun.misc.Launcher,它是一个java虚拟机的入口应用。
public class Launcher {
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
//设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
Thread.currentThread().setContextClassLoader(loader);
}
/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {}
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {}
源码有精简,我们可以得到相关的信息。
System.getProperty("sun.boot.class.path")得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。我们可以先代码测试一下sun.boot.class.path是什么内容。
System.out.println(System.getProperty("sun.boot.class.path"));
得到的结果是:
C:\Program Files\Java\jre1.8.0_91\lib\resources.jar;
C:\Program Files\Java\jre1.8.0_91\lib\rt.jar;
C:\Program Files\Java\jre1.8.0_91\lib\sunrsasign.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jsse.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jce.jar;
C:\Program Files\Java\jre1.8.0_91\lib\charsets.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jfr.jar;
C:\Program Files\Java\jre1.8.0_91\classes
可以看到,这些全是JRE目录下的jar包或者是class文件。
如果你有足够的好奇心,你应该会对它的源码感兴趣
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
/**
* create an ExtClassLoader. The ExtClassLoader is created
* within a context that limits which files it can read
*/
public static ExtClassLoader getExtClassLoader() throws IOException
{
final File[] dirs = getExtDirs();
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
......
}
我们先前的内容有说过,可以指定-D java.ext.dirs参数来添加和改变ExtClassLoader的加载路径。这里我们通过可以编写测试代码。
System.out.println(System.getProperty("java.ext.dirs"));
结果如下:
C:\Program Files\Java\jre1.8.0_91\lib\ext;C:\Windows\Sun\Java\lib\ext
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
......
}
可以看到AppClassLoader加载的就是java.class.path下的路径。我们同样打印它的值。
System.out.println(System.getProperty("java.class.path"));
结果:
D:\workspace\ClassLoaderDemo\bin
这个路径其实就是当前java工程目录bin,里面存放的是编译生成的class文件。
好了,自此我们已经知道了BootstrapClassLoader、ExtClassLoader、AppClassLoader实际是查阅相应的环境属性sun.boot.class.path、java.ext.dirs和java.class.path来加载资源文件的。
接下来我们探讨它们的加载顺序,我们先用Eclipse建立一个java工程。

然后创建一个Test.java文件。
public class Test{}
然后,编写一个ClassLoaderTest.java文件。
public class ClassLoaderTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
ClassLoader cl = Test.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());
}
}
我们获取到了Test.class文件的类加载器,然后打印出来。结果是:
ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
也就是说明Test.class文件是由AppClassLoader加载的。
这个Test类是我们自己编写的,那么int.class或者是String.class的加载是由谁完成的呢?
我们可以在代码中尝试
public class ClassLoaderTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
ClassLoader cl = Test.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());
cl = int.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());
}
}
运行一下,却报错了
ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
Exception in thread "main" java.lang.NullPointerException
at ClassLoaderTest.main(ClassLoaderTest.java:15)
提示的是空指针,意思是int.class这类基础类没有类加载器加载?
当然不是!
int.class是由Bootstrap ClassLoader加载的。要想弄明白这些,我们首先得知道一个前提。
每个类加载器都有一个父加载器,比如加载Test.class是由AppClassLoader完成,那么AppClassLoader也有一个父加载器,怎么样获取呢?很简单,通过getParent方法。比如代码可以这样编写:
ClassLoader cl = Test.class.getClassLoader();
System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());
运行结果如下:
ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@15db9742
这个说明,AppClassLoader的父加载器是ExtClassLoader。那么ExtClassLoader的父加载器又是谁呢?
System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());
System.out.println("ClassLoader\'s grand father is:"+cl.getParent().getParent().toString());
运行如果:
ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
Exception in thread "main" ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@15db9742
java.lang.NullPointerException
at ClassLoaderTest.main(ClassLoaderTest.java:13)
又是一个空指针异常,这表明ExtClassLoader也没有父加载器。那么,为什么标题又是每一个加载器都有一个父加载器呢?这不矛盾吗?为了解释这一点,我们还需要看下面的一个基础前提。
我们先前已经粘贴了ExtClassLoader和AppClassLoader的代码。
static class ExtClassLoader extends URLClassLoader {}
static class AppClassLoader extends URLClassLoader {}
可以看见ExtClassLoader和AppClassLoader同样继承自URLClassLoader,但上面一小节代码中,为什么调用AppClassLoader的getParent()代码会得到ExtClassLoader的实例呢?先从URLClassLoader说起,这个类又是什么?
先上一张类的继承关系图

URLClassLoader的源码中并没有找到getParent()方法。这个方法在ClassLoader.java中。
public abstract class ClassLoader {
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// The class loader for the system
// @GuardedBy("ClassLoader.class")
private static ClassLoader scl;
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
...
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
public final ClassLoader getParent() {
if (parent == null)
return null;
return parent;
}
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
return scl;
}
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
//通过Launcher获取ClassLoader
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
}
我们可以看到getParent()实际上返回的就是一个ClassLoader对象parent,parent的赋值是在ClassLoader对象的构造方法中,它有两个情况:
getSystemClassLoader()方法生成,也就是在sun.misc.Laucher通过getClassLoader()获取,也就是AppClassLoader。直白的说,一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。我们主要研究的是ExtClassLoader与AppClassLoader的parent的来源,正好它们与Launcher类有关,我们上面已经粘贴过Launcher的部分代码。
public class Launcher {
private static URLStreamHandlerFactory factory = new Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
//将ExtClassLoader对象实例传递进去
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
public ClassLoader getClassLoader() {
return loader;
}
static class ExtClassLoader extends URLClassLoader {
/**
* create an ExtClassLoader. The ExtClassLoader is created
* within a context that limits which files it can read
*/
public static ExtClassLoader getExtClassLoader() throws IOException
{
final File[] dirs = getExtDirs();
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
//ExtClassLoader在这里创建
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
/*
* Creates a new ExtClassLoader for the specified directories.
*/
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
}
}
}
我们需要注意的是
ClassLoader extcl;
extcl = ExtClassLoader.getExtClassLoader();
loader = AppClassLoader.getAppClassLoader(extcl);
代码已经说明了问题AppClassLoader的parent是一个ExtClassLoader实例。
ExtClassLoader并没有直接找到对parent的赋值。它调用了它的父类也就是URLClassLoder的构造方法并传递了3个参数。
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
}
对应的代码
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
super(parent);
}
答案已经很明了了,ExtClassLoader的parent为null。
上面张贴这么多代码也是为了说明AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是null。这符合我们之前编写的测试代码。
不过,细心的同学发现,还是有疑问的我们只看到ExtClassLoader和AppClassLoader的创建,那么BootstrapClassLoader呢?
还有,ExtClassLoader的父加载器为null,但是Bootstrap CLassLoader却可以当成它的父加载器这又是为何呢?
我们继续往下进行。
Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。然后呢,我们前面已经分析了,JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器。Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器。比如ExtClassLoader。这也可以解释之前通过ExtClassLoader的getParent方法获取为Null的现象。具体是什么原因,很快就知道答案了。
双亲委托。
我们终于来到了这一步了。
一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。
整个流程可以如下图所示:

这张图是用时序图画出来的,不过画出来的结果我却自己都觉得不理想。
大家可以看到2根箭头,蓝色的代表类加载器向上委托的方向,如果当前的类加载器没有查询到这个class对象已经加载就请求父加载器(不一定是父类)进行操作,然后以此类推。直到Bootstrap ClassLoader。如果Bootstrap ClassLoader也没有加载过此class实例,那么它就会从它指定的路径中去查找,如果查找成功则返回,如果没有查找成功则交给子类加载器,也就是ExtClassLoader,这样类似操作直到终点,也就是我上图中的红色箭头示例。
用序列描述一下:
sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。上面的序列,详细说明了双亲委托的加载流程。我们可以发现委托是从下向上,然后具体查找过程却是自上至下。
我说过上面用时序图画的让自己不满意,现在用框图,最原始的方法再画一次。

上面已经详细介绍了加载过程,但具体为什么是这样加载,我们还需要了解几个个重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。
JDK文档中是这样写的,通过指定的全限定类名加载class,它通过同名的loadClass(String,boolean)方法。
protected Class<?> loadClass(String name,
boolean resolve)
throws ClassNotFoundException
上面是方法原型,一般实现这个方法的步骤是
findLoadedClass(String)去检测这个class是不是已经加载过了。loadClass方法。如果父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。findClass(String)查找。如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()又会调用resolveClass(Class)这个方法来生成最终的Class对象。 我们可以从源代码看出这个步骤。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检测是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则调用Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//父加载器没有找到,则调用findclass
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//调用resolveClass()
resolveClass(c);
}
return c;
}
}
代码解释了双亲委托。
另外,要注意的是如果要编写一个classLoader的子类,也就是自定义一个classloader,建议覆盖findClass()方法,而不要直接改写loadClass()方法。
另外
if (parent != null) {
//父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则调用Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
前面说过ExtClassLoader的parent为null,所以它向上委托时,系统会为它指定Bootstrap ClassLoader。
不知道大家有没有发现,不管是Bootstrap ClassLoader还是ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。如果在某种情况下,我们需要动态加载一些东西呢?比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,这样可以吗?
如果要这样做的话,需要我们自定义一个classloader。
findClass()方法。findClass()方法中调用defineClass()。这个方法在编写自定义classloader的时候非常重要,它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常。
**一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。 **
上面说的是,如果自定义一个ClassLoader,默认的parent父加载器是AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的class文件。
假设我们需要一个自定义的classloader,默认加载路径为D:\lib下的jar包和资源。
我们写编写一个测试用的类文件,Test.java
package com.frank.test;
public class Test {
public void say(){
System.out.println("Say Hello");
}
}
然后将它编译过年class文件Test.class放到D:\lib这个路径下。
我们编写DiskClassLoader的代码。
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class DiskClassLoader extends ClassLoader {
private String mLibPath;
public DiskClassLoader(String path) {
// TODO Auto-generated constructor stub
mLibPath = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
String fileName = getFileName(name);
File file = new File(mLibPath,fileName);
try {
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
try {
while ((len = is.read()) != -1) {
bos.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close();
bos.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return super.findClass(name);
}
//获取要加载 的class文件名
private String getFileName(String name) {
// TODO Auto-generated method stub
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
我们在findClass()方法中定义了查找class的方法,然后数据通过defineClass()生成了Class对象。
现在我们要编写测试代码。我们知道如果调用一个Test对象的say方法,它会输出"Say Hello"这条字符串。但现在是我们把Test.class放置在应用工程所有的目录之外,我们需要加载它,然后执行它的方法。具体效果如何呢?我们编写的DiskClassLoader能不能顺利完成任务呢?我们拭目以待。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClassLoaderTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建自定义classloader对象。
DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
try {
//加载class文件
Class c = diskLoader.loadClass("com.frank.test.Test");
if(c != null){
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod("say",null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
我们点击运行按钮,结果显示。

可以看到,Test类的say方法正确执行,也就是我们写的DiskClassLoader编写成功。
讲了这么大的篇幅,自定义ClassLoader才姗姗来迟。 很多同学可能觉得前面有些啰嗦,但我按照自己的思路,我觉得还是有必要的。因为我是围绕一个关键字进行讲解的。
关键字是什么?
它们的关联部分就是路径,也就是要加载的class或者是资源的路径。
BootStrap ClassLoader、ExtClassLoader、AppClassLoader都是加载指定路径下的jar包。如果我们要突破这种限制,实现自己某些特殊的需求,我们就得自定义ClassLoader,自已指定加载的路径,可以是磁盘、内存、网络或者其它。
所以,你说路径能不能成为它们的关键字?
当然上面的只是我个人的看法,可能不正确,但现阶段,这样有利于自己的学习理解。
突破了JDK系统内置加载路径的限制之后,我们就可以编写自定义ClassLoader,然后剩下的就叫给开发者你自己了。你可以按照自己的意愿进行业务的定制,将ClassLoader玩出花样来。
常见的用法是将Class文件按照某种加密手段进行加密,然后按照规则编写自定义的ClassLoader进行解密,这样我们就可以在程序中加载特定了类,并且这个类只能被我们自定义的加载器进行加载,提高了程序的安全性。
下面,我们编写代码。
加密和解密的协议有很多种,具体怎么定看业务需要。在这里,为了便于演示,我简单地将加密解密定义为异或运算。当一个文件进行异或运算后,产生了加密文件,再进行一次异或后,就进行了解密。
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileUtils {
public static void test(String path){
File file = new File(path);
try {
FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(path+"en");
int b = 0;
int b1 = 0;
try {
while((b = fis.read()) != -1){
//每一个byte异或一个数字2
fos.write(b ^ 2);
}
fos.close();
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
我们再写测试代码
FileUtils.test("D:\\lib\\Test.class");

然后可以看见路径D:\\lib\\Test.class下Test.class生成了Test.classen文件。
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class DeClassLoader extends ClassLoader {
private String mLibPath;
public DeClassLoader(String path) {
// TODO Auto-generated constructor stub
mLibPath = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
String fileName = getFileName(name);
File file = new File(mLibPath,fileName);
try {
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
byte b = 0;
try {
while ((len = is.read()) != -1) {
//将数据异或一个数字2进行解密
b = (byte) (len ^ 2);
bos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close();
bos.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return super.findClass(name);
}
//获取要加载 的class文件名
private String getFileName(String name) {
// TODO Auto-generated method stub
int index = name.lastIndexOf('.');
if(index == -1){
return name+".classen";
}else{
return name.substring(index+1)+".classen";
}
}
}
我们可以在ClassLoaderTest.java中的main方法中如下编码:
DeClassLoader diskLoader = new DeClassLoader("D:\\lib");
try {
//加载class文件
Class c = diskLoader.loadClass("com.frank.test.Test");
if(c != null){
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod("say",null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
查看运行结果是:

可以看到了,同样成功了。现在,我们有两个自定义的ClassLoader:DiskClassLoader和DeClassLoader,我们可以尝试一下,看看DiskClassLoader能不能加载Test.classen文件也就是Test.class加密后的文件。
我们首先移除D:\\lib\\Test.class文件,只剩下一下Test.classen文件,然后进行代码的测试。
DeClassLoader diskLoader1 = new DeClassLoader("D:\\lib");
try {
//加载class文件
Class c = diskLoader1.loadClass("com.frank.test.Test");
if(c != null){
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod("say",null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
try {
//加载class文件
Class c = diskLoader.loadClass("com.frank.test.Test");
if(c != null){
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod("say",null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
运行结果:

我们可以看到。DeClassLoader运行正常,而DiskClassLoader却找不到Test.class的类,并且它也无法加载Test.classen文件。
前面讲到过Bootstrap ClassLoader、ExtClassLoader、AppClassLoader,现在又出来这么一个类加载器,这是为什么?
前面三个之所以放在前面讲,是因为它们是真实存在的类,而且遵从”双亲委托“的机制。而ContextClassLoader其实只是一个概念。
查看Thread.java源码可以发现
public class Thread implements Runnable {
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
}
contextClassLoader只是一个成员变量,通过setContextClassLoader()方法设置,通过getContextClassLoader()设置。
每个Thread都有一个相关联的ClassLoader,默认是AppClassLoader。并且子线程默认使用父线程的ClassLoader除非子线程特别设置。
我们同样可以编写代码来加深理解。
现在有2个SpeakTest.class文件,一个源码是
package com.frank.test;
public class SpeakTest implements ISpeak {
@Override
public void speak() {
// TODO Auto-generated method stub
System.out.println("Test");
}
}
它生成的SpeakTest.class文件放置在D:\\lib\\test目录下。
另外ISpeak.java代码
package com.frank.test;
public interface ISpeak {
public void speak();
}
然后,我们在这里还实现了一个SpeakTest.java
package com.frank.test;
public class SpeakTest implements ISpeak {
@Override
public void speak() {
// TODO Auto-generated method stub
System.out.println("I\' frank");
}
}
它生成的SpeakTest.class文件放置在D:\\lib目录下。
然后我们还要编写另外一个ClassLoader,DiskClassLoader1.java这个ClassLoader的代码和DiskClassLoader.java代码一致,我们要在DiskClassLoader1中加载位置于D:\\lib\\test中的SpeakTest.class文件。
测试代码:
DiskClassLoader1 diskLoader1 = new DiskClassLoader1("D:\\lib\\test");
Class cls1 = null;
try {
//加载class文件
cls1 = diskLoader1.loadClass("com.frank.test.SpeakTest");
System.out.println(cls1.getClassLoader().toString());
if(cls1 != null){
try {
Object obj = cls1.newInstance();
//SpeakTest1 speak = (SpeakTest1) obj;
//speak.speak();
Method method = cls1.getDeclaredMethod("speak",null);
//通过反射调用Test类的speak方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());
// TODO Auto-generated method stub
try {
//加载class文件
// Thread.currentThread().setContextClassLoader(diskLoader);
//Class c = diskLoader.loadClass("com.frank.test.SpeakTest");
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class c = cl.loadClass("com.frank.test.SpeakTest");
// Class c = Class.forName("com.frank.test.SpeakTest");
System.out.println(c.getClassLoader().toString());
if(c != null){
try {
Object obj = c.newInstance();
//SpeakTest1 speak = (SpeakTest1) obj;
//speak.speak();
Method method = c.getDeclaredMethod("speak",null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException
| SecurityException |
IllegalArgumentException |
InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
结果如下:

我们可以得到如下的信息:
我们修改一下代码,在子线程开头处加上这么一句内容。
Thread.currentThread().setContextClassLoader(diskLoader1);
结果如下:

可以看到子线程的ContextClassLoader变成了DiskClassLoader。
继续改动代码:
Thread.currentThread().setContextClassLoader(diskLoader);
结果:

可以看到DiskClassLoader1和DiskClassLoader分别加载了自己路径下的SpeakTest.class文件,并且它们的类名是一样的com.frank.test.SpeakTest,但是执行结果不一样,因为它们的实际内容不一样。
其实这个我也不是很清楚,我的主业是Android,研究ClassLoader也是为了更好的研究Android。网上的答案说是适应那些Web服务框架软件如Tomcat等。主要为了加载不同的APP,因为加载器不一样,同一份class文件加载后生成的类是不相等的。如果有同学想多了解更多的细节,请自行查阅相关资料。
我这篇文章写了好几天,修修改改,然后加上自己的理解。参考了下面的这些网站。
现阶段,找个方便好使的编程环境还是比较蛋疼的,对于部分想过瘾或者想从学习实践中学习的小伙伴来说,略显蛋疼。不过,仔细琢磨,还是能够自己折腾出一个好用的环境来的。开搞。
也就是说,你先安装一个能正常使用的Ubuntu再说吧,然后顺便熟悉一些相关的概念和操作。
后面若没有特殊说明,那我们讨论的问题都是在这个软件环境下。
先打开终端,安装所需软件(注意$开头的才是命令,并且$并不属于命令的一部分):
$ sudo apt-get install gcc nasm vim gcc-multilib -y
在终端中分别执行which nasm和which gcc,得到如下结果,则表示环境已经安装完毕。
$ which nasm
/usr/bin/nasm
$ which gcc
/usr/bin/gcc
在汇编语言环境下,我们先别急着搞什么Hello World,在这里要打印出Hello World还不是一个简单的事情,这也算是初入汇编比较让人不解的地方,成天都在扯什么寄存器寻址啥的,说好的变量分支循环函数呢?
别说话,先按照我的套路把环境配好,程序跑起来了再说。注意,不是Hello World。先亮出第一个程序的C语言等价代码:
int main() {
return 0;
}
不好意思,大括号没换行。你以为接下来我要gcc -S吗?Too naive。我这可是正宗手工艺,非机械化生产。
说正事,先一股脑啥都不知道地把代码敲完,跑起来再说:
首先准备个文件,暂且叫做first.asm吧,然后把下面的代码搞进去:
global main
main:
mov eax, 0
ret
好了程序写完了,你能感受到这里的0就是上面C代码里的0,说明你有学习汇编的天赋。
OK接下来就要编译运行了。来一堆命令先:
$ nasm -f elf first.asm -o first.o
$ gcc -m32 first.o -o first
这下,程序就编译好了,像这样:
$ ls
first first.asm first.o
好了我们运行一下:
$ ./first ; echo $?
别问我为何上面的命令后面多了一串奇怪的代码,你自己把它删掉之后再看就能猜出来是干啥的了。如果还有疑惑,可以再次做实验确认,比如把代码里的0改成1。变成这样:
global main
main:
mov eax, 1
ret
再按照同样的套路来编译运行:
$ nasm -f elf first.asm -o first.o
$ gcc -m32 first.o -o first
$ ./first ; echo $?
1
OK,咱们的环境准备工作大功告成,后面再细说该怎么搞事情(心情好的话还有ARM版的哦,准备好ARM环境或者买个树莓派吧)。
上回说到,咱们把环境搭好了,可以开始玩耍汇编了。
开始学C的时候,有没有一种感觉,变量?类型?我可是要改变世界的男人,怎么就成天在跟i++较劲啊?这黑框程序还只能用来算数学,跟说好的不一样呢???想必后来,见得多了,你的想法也不那么幼稚了吧。
好了,接下来汇编也会给你同样一种感觉的。啥玩意儿?寄存器?寻址?说好的变量类型循环函数呢?。
好了,我想先刻意避开这些晦涩难懂的东西,找到感觉了,再回头来研究条条框框。在此先把基本的几个简单的东西弄熟练,过早引入太多概念容易让人头昏眼花。
这里就说寄存器,通俗地来解释一下。回到上小学的时候,现在有一大堆计算题:
99+10=
32-20=
14+21=
47-9=
87+3=
86-8=
...
正常来讲,要算出这个么多题目,你需要一支笔,一边计算的同时一边把结果写下来。
好了,到这里,我就来做个类比,助你大致理解寄存器是干啥用的。首先,我们把CPU和大脑做一个类比。
你在纸上进行计算的时候,需要不断往纸上写下计算结果,一边在脑子里进行计算。大致过程就像:
1. 在纸上找一个题目,先看清两个数字,并迅速记下来
2. 在脑子里对这两个数字进行计算,计算出的结果也是记在脑子里的
3. 将计算结果写在纸上,继续做下一个题目
好了,这个过程就和计算机执行的过程有几分相似。草稿纸就相当于是个内存,脑子就是CPU。
在计算的时候,你需要知道计算的两个数字,而且还得知道是做什么运算,这些信息都是从草稿纸上看见之后,短暂记忆在脑子里的。
CPU在计算的时候也是一样,需要知道要计算的数据是什么,还得知道是做什么运算,这些信息也需要临时保存在CPU的某个地方。这个地方就是寄存器。
好了到这里,不知道你有没有看明白?也就是说CPU里头的寄存器的作用,就像我们在做计算的时候会临时在脑子里记住数字一样。当然你的脑子能记住不止一个数据,CPU也不止一个寄存器。
就是因为写汇编语言的时候,要在有限的寄存器情况下,编写复杂的程序,还要考虑灵活性、性能、正确性等等乱七八糟的问题,对于程序员是一个超级大的负担。
因此有人专门发明了许多更方便好用的“高级语言”,然后还专门写了个配套的程序能够把用这个“高级语言”写的东西翻译成汇编语言,再将汇编语言翻译成机器能执行的指令。其中之一就是C语言。也就是C语言发明出来就是奔着比汇编语言好用的目标去的。所以啊,相比汇编这种繁琐复杂的编程方式,高级语言不知道高级到哪里去了。
没有。
好了,先介绍个程序,运行完了能够开心一下:
global main
main:
mov eax, 1
mov ebx, 2
add eax, ebx
ret
老套路,保存成文件,比如叫做nmb.asm,然后编译运行:
$ nasm -f elf nmb.asm -o nmb.o
$ gcc -m32 nmb.o -o nmb
$ ./nmb ; echo $?
3
如果你能看出来这里面的端倪,说明你是一个聪明伶俐的天才。不就是做了个算术题1+2=3么。
好了我们来看一下这个程序。里面的eax就是指代的寄存器。同理ebx也是一个寄存器。也就是这个CPU在做计算题的时候至少能够记住两个数字,实际上,它有更多寄存器,稍后再慢慢说。
OK。既然找到一些感觉了,就继续胡乱地拍出一大堆程序来先玩个够吧:
global main
main:
mov eax, 1
add eax, 2
add eax, 3
add eax, 4
add eax, 5
ret
global main
main:
mov eax, 1
mov ebx, 2
mov ecx, 3
mov edx, 4
add eax, ebx
add eax, ecx
add eax, edx
ret
至于这两个程序是什么结果,你自己玩吧。不动手练练怎么学得好。
指令就像是你发给CPU的一个个命令,你让它做啥它就做啥。当然了,前提是CPU得支持对应的功能,CPU是没有“吃饭”功能的,你也写不出让它”吃饭“的指令来。
前面我们共用到了三个指令:分别是mov、add、ret。
我来逐个解释这些指令:
数据传送指令,我们可以像下面这样用mov指令,达到数据传送的目的。
mov eax, 1 ; 让eax的值为1(eax = 1)
mov ebx, 2 ; 让ebx的值为2(ebx = 2)
mov ecx, eax ; 把eax的值传送给ecx(ecx = eax)
加法指令
add eax, 2 ; eax = eax + 2
add ebx, eax ; ebx = ebx + eax
返回指令,类似于C语言中的return,用于函数调用后的返回(后面细说)。
首先,CPU里是一坨电路,有的功能对于人来说可能很简单,但是对于想要用电路来实现这个功能的人来说,就不一定简单了。这是需要明白的第一个道理。
所以啊,这长得丑是有原因的。其中一个原因就是,某些长的漂亮的功能用电路实现起来超级麻烦,所以干脆设计丑一点,反正到时候这些古怪的指令能够组合出我想要的功能,也就足够了。
所以,汇编语言蛋疼就在这些地方:
按道理,随着技术的发展,指令应该越来越好看,越来越符合人的思考方式才对啊。然而,世事难料,自从出现了高级语言,多数编程场景下,已经不需要关心指令和寄存器到底长啥样了,这个事情已经由编译器代劳了,99%甚至更多的程序员不需关心寄存器和指令了。所以,长得不好看就算了,反正也没什么人看。
好了,按照前面的介绍,接下来再继续了解一些东西:
减法指令(用法和加法指令类似)
sub eax, 1 ; eax = eax - 1
sub eax, ecx ; eax = eax - ecx
乘法和除法、以及更多的运算,这里就不再介绍了,这里的重点是为汇编学习带路。
除了前面列举的eax、ebx、ecx、edx之外,还有一些寄存器:
esi
edi
ebp
其中eax、ebx、ecx、edx这四个寄存器是通用寄存器,可以随便存放数据,也能参与到大多数的运算。而余下的三个多见于一些访问内存的场景下,不过,目前,你还是可以随便抓住一个就拿来用的。
到这里,赶紧根据前面了解的东西,多写几遍吧,加深一下印象。
前面说的学习汇编没用,是瞎说的。学习汇编有用,后面想起来了再说。
上回说到了寄存器和指令,这回说下内存访问。开始之前,先来复习一下。
高能预警,后面会涉及到一些高难度动作,请提前做好以下准备:
举个例子,一个16进制数0BC71820,其二进制表示为:
00001011 11000111 00011000 00100000
你能快速地找到它们之间的对应关系吗?不会的话快去复习吧。
现在,为了简便,我们只讨论32位宽的寄存器。也就是说,目前我们讨论的寄存器,它的宽度都是32位的,也就是里面存放了一个32位长的2进制数。
通常,一个字节为8个二进制比特位,那么一个32位长的二进制数,那么它的大小就应该是4个字节。也就是把32位长的寄存器写入到内存里,会覆盖掉四个字节的存储空间。
想必内存大家心里都比较有数,就是暂时存放CPU计算所需的指令和数据的地方。
诶?那前面说好的寄存器呢?寄存器也是类似的功能啊。对的,寄存器有类似功能,理论上一个最小的计算系统只需要寄存器和CPU的计算部件(ALU)就够了。不过,实际情况更加复杂一些,还是拿计算题举例,这次更复杂了:
(这里的例子只够说明寄存器和内存的角色区别,而非出现内存和寄存器这样角色的根本原因)
( 847623785 * 12874873 + 274632 ) / 999 =
好了,这个题目就不像前面的那么简单了,首先你肯定没法直接在脑子里三两下就算出来,还是得需要一个草稿纸了。
计算过程中,你还是会把草稿纸上正在计算的几个数字记在脑子里,然后快速地算完并记下来,然后往草稿纸上写。
最后,在草稿纸上演算完毕后,你会把最终结果写到试卷上。
好了,这里的草稿纸就相当于是内存了。它也充当一个临时记录数据的作用,不过它的容量就比自己的脑子要大得多了,而且一旦你把东西写下来,也就不那么担心忘记了。
诶?我不能多做点寄存器,就不需要单独的内存了呀?是的,理论上是这样,然而,实际上如果多做一点寄存器的话,CPU就要卖$9999999一片了,贵啊(具体原因可以了解SRAM与DRAM)。
也就是说,在计算机系统里,寄存器和内存都充当临时存储用,但是寄存器太小也太少了,内存就能帮个大忙了。
在C语言里面,有个神奇的东西叫做指针,它是初学者的噩梦,也是高手的天堂。
这里不打算给不明白指针的人讲个明白,直接进入正题。首先,内存是一个比较大的存储器,里面可以存放非常非常多的字节。
好了,现在我们来为整个内存的所有字节编号,为了方便,咱们首先考虑按照字节为单位连续编号:
0 1 2 3 4 5 6 7 ...
......................... ......................
|12|b7|33|e8|66|4c|87|3c| ... |cc|cc|cc|cc|cc|cd|cd|
````````````````````````` ``````````````````````
大概意思一下,你可以想象每一个格子就是一个字节,每个格子都有编号,相邻的格子的编号也是相邻的。这个编号,你就可以理解为所谓的指针或者地址(这里不严格区分指针与地址)。那么当我需要获取某个位置的数据时,那么我们只需要一个编号(也就是地址)就知道在哪些格子里获取数据了,当然,写入数据也是一样的道理。
到这里,我们大概清楚了访问内存的时候需要一些什么东西:
那,我哪知道地址是多少呢?别介,这不是重点,你不需要知道地址具体是多少,你只需要知道它是个地址,按照正确的方式去思考和使用就行了。继续。
前面说到,寄存器可以临时存储计算所需数据和结果,那么,问题来了,寄存器也就那么几个,用完了咋办?你能发现这个问题,说明你有成为大佬的潜质。接下来,说正事。
前面说到了mov指令,可以将数据送入寄存器,也可以将一个寄存器的数据送到另一个寄存器,像这样:
mov eax, 1
mov ebx, eax
好了,这还没完,mov指令可谓是x86中花样比较多的指令了,前面的两种情形都还是比较简单的情形,今天我们来扯一下更复杂的。
现在,某个很复杂的运算让你感觉寄存器不够用了,怎么办?按照前面说的意思,要把寄存器的东西放到内存里去,把寄存器的空间腾出来,就可以了。
好的思路有了,可是,怎么把寄存器的数据丢到内存里去呢?还是使用mov指令,只是写法不同了:
mov [0x5566], eax
好了,现在,请全神贯注。这条指令就是将寄存器的数据丢到内存里去。再多看几眼,免得看得不够顺眼:
mov [0x0699], eax
mov [0x0998], ebx
mov [0x1299], ecx
mov [0x1499], edx
mov [0x1999], esi
好了,应该已经脸熟了。
现在,我告诉你,最前面那个指令mov [0x5566], eax的作用:
将eax寄存器的值,保存到编号为0x5566对应的内存里去,按照前面的说法,一个eax需要4个字节的空间才装得下,所以编号为0x5566 0x5567 0x5568 0x5569这四个字节都会被eax的某一部分覆盖掉。
好了,我们已经了解了如何将一个寄存器的值保存到内存里去,那么我怎么把它取出来呢?
mov eax, [0x0699]
mov ebx, [0x0998]
mov ecx, [0x1299]
mov edx, [0x1499]
mov esi, [0x1999]
反过来写就是了,比如mov eax, [0x0699]就表示把0x0699这个地址对应那片内存区域中的后4个字节取出来放到eax里面去。
到这,我们已经学会了如何把寄存器的数据临时保存到内存里,也知道怎么把内存里的数据重新放回寄存器了。
接下来,该动手操练了。先来一个题目:
假设我们现在有一个比较蛋疼的要求,就是把1和2相加,然后把结果放到内存里面,最后再把内存里的结果取出来。(好无聊的题目)
那么按理说,我们就应该这么写代码:
global main
main:
mov ebx, 1
mov ecx, 2
add ebx, ecx
mov [0x233], ebx
mov eax, [0x233]
ret
好了,编译运行,假如程序是danteng,那么运行结果应该是这样:
$ ./danteng ; echo $?
3
实际上,并不能行。程序挂了,没有输出我们想要的结果。
这是在逗我呢?别急,按理说,前面说的都是没问题的,只是这里有另外一个问题,那就是“我们的程序运行在一个受管控的环境下,是不能随便读写内存的”。这里需要特殊处理一下,至于具体为何,后面有机会再慢慢叙述,这不是当下的重点,先照抄就是了。
程序应该改成这样才行:
global main
main:
mov ebx, 1
mov ecx, 2
add ebx, ecx
mov [sui_bian_xie], ebx
mov eax, [sui_bian_xie]
ret
section .data
sui_bian_xie dw 0
好了这下运行,我们得到了结果:
$ ./danteng ; echo $?
3
好了,有了程序,咱们来梳理一下每一条语句的功能:
mov ebx, 1 ; 将ebx赋值为1
mov ecx, 2 ; 将ecx赋值为2
add ebx, ecx ; ebx = ebx + ecx
mov [sui_bian_xie], ebx ; 将ebx的值保存起来
mov eax, [sui_bian_xie] ; 将刚才保存的值重新读取出来,放到eax中
ret ; 返回,整个程序最后的返回值,就是eax中的值
好了,到这里想必你基本也明白是怎么一回事了,有几点需要专门注意的:
与前面那个崩溃的程序相比,后者有一些微小的变化,还多了两行代码
section .data
sui_bian_xie dw 0
第一行先不管是表示接下来的内容经过编译后,会放到可执行文件的数据区域,同时也会随着程序启动的时候,分配对应的内存。
第二行就是描述真实的数据的关键所在里,这一行的意思是开辟一块4字节的空间,并且里面用0填充。这里的dw(double word)就表示4个字节,前面那个sui_bian_xie的意思就是这里可以随便写,也就是起个名字而已,方便自己写代码的时候区分,这个sui_bian_xie会在编译时被编译器处理成一个具体的地址,我们无需理会地址具体时多少,反正知道前后的sui_bian_xie指代的是同一个东西就行了。
好了,有了这一个程序作铺垫,我们继续。趁热打铁,继续写代码,分析代码:
global main
main:
mov ebx, [number_1]
mov ecx, [number_2]
add ebx, ecx
mov [result], ebx
mov eax, [result]
ret
section .data
number_1 dw 10
number_2 dw 20
result dw 0
好了,自己琢磨着写代码,运行程序,然后分析程序每一条指令都在干什么。还有,这个程序本身还可以精简,如果你已经发现了,那说明你老T*棒了。
global main
main:
mov eax, [number_1]
mov ebx, [number_2]
add eax, ebx
ret
section .data
number_1 dw 10
number_2 dw 20
好了,好好分析比较上面的几个程序,基本这一块就了解得差不多了。随着了解的逐渐深入,我们后续还会介绍更多更复杂,更全面的内容。
这里插播一段反汇编的讲解。引入调试器和反汇编工具,我们后续将有更多机会对程序进行深入的分析,现阶段,我们先找一个简单的程序上手,熟悉一下操作和工具。
先安装gdb:
$ sudo apt-get install gdb -y
然后,我们把这个程序,保存为test.asm:
global main
main:
mov eax, 1
mov ebx, 2
add eax, ebx
ret
然后编译:
$ nasm -f elf test.asm -o test.o ; gcc -m32 test.o -o test
运行:
$ ./test ; echo $?
3
OK,到这里,程序是对的了。开始动刀子,使用gdb:
$ gdb ./test
启动之后,你会看到终端编程变成这样了:
(gdb)
OK,说明你成功了,接下来输入,并回车:
这里要先run一次,才能开辟真正的内存空间,只要不退出本次gdb,不论run多少次,都还是占用这些内存地址
(gdb) run
Starting program: /home/vagrant/code/asm/03/test
[Inferior 1 (process 408757) exited with code 03]
(gdb) set disassembly-flavor intel
这一步是把反汇编的格式调整称为intel的格式,稍后完事儿后你可以尝试不用这个设置,看看是什么效果。好了,继续,反汇编,输入命令并回车:
(gdb) disas main
Dump of assembler code for function main:
0x080483f0 <+0>: mov eax,0x1
0x080483f5 <+5>: mov ebx,0x2
0x080483fa <+10>: add eax,ebx
0x080483fc <+12>: ret
0x080483fd <+13>: xchg ax,ax
0x080483ff <+15>: nop
End of assembler dump.
(gdb)
好了,整个程序就在这里被反汇编出来了,请你先仔细看一看,是不是和我们写的源代码差不多?(后面多了两行汇编,你把它们当成路人甲看待就行了,不用理它)。
后面将继续介绍动态调试,帮助更加深入地理解汇编中的一些概念。现在先提示一些概念:
好了,我们开始尝试一下调试功能,首先是设置一个断点,让程序执行到某一个地方就停下来,给我们足够的时间观察。在gdb的命令行中输入:
(gdb) break *0x080483f5
后面那串奇怪的数字在不同的环境下可能不一样,你可以结合这里的代码,对照着自己的实际情况修改。(使用反汇编中<+5>所在的那一行前面的数字)
然后我们执行程序:
(gdb) run
Starting program: /home/vagrant/code/asm/03/test
Breakpoint 1, 0x080483f5 in main ()
(gdb)
看到了吧,这下程序就被停在了我们设置的断点那个地方,对比着反汇编和你的汇编代码,找一找现在程序是停在哪个位置的吧。run后面提示的内容里,那一串奇怪的数字又出现了,其实这就是我们前面设置断点的那个地址。
好了,到这里,我们就把程序看个精光吧,先看一下eax寄存器的值:
(gdb) info register eax
eax 0x1 1
刚好就是1啊,在我们设置断点的那个地方,它的前面一个指令是mov eax, 1,这时候eax的内容就真的变成1了,同样,你还可以看一下ebx:
info register ebx
ebx 0xf7fce000 -134422528
ebx的值并不是2,这是因为mov ebx, 2这个语句还没有执行,所以暂时你看不到。那我们现在让它执行一下吧:
(gdb) stepi
0x080483fa in main ()
好了,输入stepi之后,到这里,程序在我们的控制之下,向后运行了一条指令,也就是刚刚执行了mov ebx, 2,这时候看下ebx:
(gdb) info register ebx
ebx 0x2 2
看到了吧,ebx已经变成2了。继续,输入stepi,然后看执行了add指令后的各个寄存器的值:
(gdb) stepi
0x080483fc in main ()
(gdb) info register eax
eax 0x3 3
执行完add指令之后,eax跟我们想的一样,变成了3。如果我不知道程序现在停在哪里了,怎么办?很简单,输入disas之后,又能看到反汇编了,同时gdb还会标记出当前断点所在的位置:
(gdb) disas
Dump of assembler code for function main:
0x080483f0 <+0>: mov eax,0x1
0x080483f5 <+5>: mov ebx,0x2
0x080483fa <+10>: add eax,ebx
=> 0x080483fc <+12>: ret
0x080483fd <+13>: xchg ax,ax
0x080483ff <+15>: nop
End of assembler dump.
现在刚好就在add执行过后的ret那个地方。这时候,如果你不想玩了,可以输入continue,让程序自由地飞翔起来,直到GG。
(gdb) continue
Continuing.
[Inferior 1 (process 1283) exited with code 03]
看到了吧,程序已经GG了,而且返回了一个数字03。这刚好就是那个eax寄存器的值嘛。
完整的过程
注意进入gdb后一定要先run一次,这样才会开辟真正的内存空间,之后再看内存分配和设置断点才好用
koala@koala:~/桌面/DesktopHelper/汇编语言入门/03demo$ gdb ./test
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./test...
(No debugging symbols found in ./test)
(gdb) run
Starting program: /media/koala/data/WinUserHome/DesktopFile/DesktopHelper/汇编语言入门/03demo/test
[Inferior 1 (process 408757) exited with code 03]
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x565561a0 <+0>: mov eax,0x1
0x565561a5 <+5>: mov ebx,0x2
0x565561aa <+10>: add eax,ebx
0x565561ac <+12>: ret
0x565561ad <+13>: xchg ax,ax
0x565561af <+15>: nop
End of assembler dump.
(gdb) break *0x565561a5
Breakpoint 1 at 0x565561a5
(gdb) run
Starting program: /media/koala/data/WinUserHome/DesktopFile/DesktopHelper/汇编语言入门/03demo/test
Breakpoint 1, 0x565561a5 in main ()
(gdb) info register eax
eax 0x1 1
(gdb) info register ebx
ebx 0x0 0
(gdb) stepi
0x565561aa in main ()
(gdb) info register ebx
ebx 0x2 2
(gdb) stepi
0x565561ac in main ()
(gdb) info register eax
eax 0x3 3
(gdb) disas
Dump of assembler code for function main:
0x565561a0 <+0>: mov eax,0x1
0x565561a5 <+5>: mov ebx,0x2
0x565561aa <+10>: add eax,ebx
=> 0x565561ac <+12>: ret
0x565561ad <+13>: xchg ax,ax
0x565561af <+15>: nop
End of assembler dump.
(gdb) continue
Continuing.
[Inferior 1 (process 409281) exited with code 03]
好了,这次就到这里结束,内容有点多,没关系可以慢慢来,没事的时候就翻出来,把目前学的汇编语言和gdb都好好玩一下,最好是能玩出花来,这样才能有更多的收获。清点一下今天的内容:
若读者对文中部分内容有疑惑或是有表达不当或是有疏漏,欢迎指正。
上回我们把汇编里涉及到的寄存器和内存访问相关的内容说了。先来梳理一下:
还有一些疑虑,先暂时解释一下。首先,C语言里编程里,我们从来没有关心过寄存器。汇编语言里突然冒出这么一个东西,学起来好难受。接下来的内容,我们先把C语言和汇编语言的知识,来一次大一统,帮助理解。
首先我们来看一个C语言程序:
int x, y, z;
int main() {
x = 2;
y = 3;
z = x + y;
return z;
}
考虑到我们的汇编教程才刚开始,我这里尽可能先简化C程序,这样稍后涉及到等价的汇编内容时所需的知识都是前面介绍过的。
保存为test01.c文件,先编译运行这个程序:
(注意,这里的gcc带了一个参数-m32,因为我们要编译出32位(x86)的可执行文件)
$ gcc -m32 test01.c -o test01
$ ./test01 ; echo $?
5
好了,在这里,我们的程序返回了一个值:5。
好的,接下来我们看看如果我们要用汇编实现几乎相同的过程,该怎么做?
首先,三个全局变量:
int x, y, z;
总得有吧。(这里之所以会用全局变量,是考虑到局部变量相关的汇编知识还未介绍,先将就一下,后续再说局部变量的内容)
首先,在C语言里,你可以认为每个变量都会占用一定的内存空间,也就是说,这里的x、y、z分别都占用了一个“整型”也就是4字节的存储空间。
上次我们介绍过在汇编里面访问内存的知识,当然,我们也知道了怎么在数据区划出一定的空间,这次我们就照搬前面提及的方法:
global main
main:
mov eax, 0
ret
section .data
x dw 0
y dw 0
z dw 0
这个程序就等价于下面的C代码:
int x, y, z;
int main() {
return 0;
}
也就是现在有了三个全局变量,只是现在汇编程序什么都没做,仅仅返回了0而已。
这里的C代码和上述汇编代码从某种程度上来说,就是完全等价的。甚至,我们的C语言编译器就可以直接把C代码,翻译成上述的汇编代码,余下的工作交给nasm再编译一次,把汇编转化为可执行文件,就能够得到最后的程序了。当然,理论上可以这么做,实际上有的编译器也就是这么做的,只是人家生成的汇编格式不是nasm,而是其它的类型,但是道理都差不多。
也就是说,一个足够精简的C编译器,只需要能够把C代码翻译成汇编代码,剩下的交给汇编器完成,也就能实现完整的C语言编译器了,也就能得到最后的可执行文件了。实际上C编译器是完全可以这么做的,甚至有的就是这么做的。
好了,先不扯这些,我们先把前面的程序补充完整,达到和最前面的C代码等价为止。接下来,我们要关注这个:
x = 2;
y = 3;
也就是要把数字2和3,分别放到x和y对应的内存区域中去。很简单,我们可以这么做:
mov eax, 2
mov [x], eax
mov eax, 3
mov [y], eax
也就是先把2扔到寄存器eax中去,然后把eax中的内容放回到x对应的内存中。同理,y也这样处理。
好了,接下来的加法语句:
z = x + y;
也可以做了:
mov eax, [x]
mov ebx, [y]
add eax, ebx
mov [z], eax
好了,这段代码应该可以看懂吧,简单说一下思路:
最后,我们还有一个事情需要处理,也就是返回语句:
return z;
这个也很好办,按照约定,eax中的值,就是函数的返回值:
mov eax, [z]
ret
整个程序就算完了,我们已经完整地将C代码的汇编语言等价形式写出来了,最终的代码是这样的:
global main
main:
mov eax, 2
mov [x], eax
mov eax, 3
mov [y], eax
mov eax, [x]
mov ebx, [y]
add eax, ebx
mov [z], eax
mov eax, [z]
ret
section .data
x dw 0
y dw 0
z dw 0
来先保存成文件test02.asm,编译运行看看效果:
$ nasm -f elf test02.asm -o test02.o
$ gcc -m32 test02.o -o test02
$ ./test02 ; echo $?
5
搞定。结果完全和前面的C代码一致。
你以为自己YY出等价的汇编代码就完事儿了?图样,接下来我们继续用工具一探究竟,玩真的。
先说一下准备工作,首先有下面两个文件:
test01.c test02.asm
其中一个为上面提到的完整C代码,一个为上述完整的汇编代码。然后按照前面的指示,都编译成可执行文件,编译完成后是这样的:
$ gcc -m32 test01.c -o test01
$ nasm -f elf test02.asm -o test02.o
$ gcc -m32 -fno-lto test02.o -o test02
$ ls
test01 test01.c test02 test02.asm test02.o
(注意,要按照这里的编译命令来做)
其中的test01是C代码编译出来的,test02是汇编代码编译出来的。
好,接下来有请我们的大将军gdb登场。
先来看看我们的C编译后的程序,反汇编之后是什么鬼样子:
gdb ./test01
然后输入命令查看反汇编代码:
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x080483ed <+0>: push ebp
0x080483ee <+1>: mov ebp,esp
0x080483f0 <+3>: mov DWORD PTR ds:0x804a024,0x2
0x080483fa <+13>: mov DWORD PTR ds:0x804a028,0x3
0x08048404 <+23>: mov edx,DWORD PTR ds:0x804a024
0x0804840a <+29>: mov eax,ds:0x804a028
0x0804840f <+34>: add eax,edx
0x08048411 <+36>: mov ds:0x804a020,eax
0x08048416 <+41>: mov eax,ds:0x804a020
0x0804841b <+46>: pop ebp
0x0804841c <+47>: ret
End of assembler dump.
(gdb) quit
$
好,别急,先退出,我们再看看我们汇编程序的反汇编代码:
gdb ./test02
(gdb) set disassembly-flavor intel
(gdb) disas main
0x080483f0 <+0>: mov eax,0x2
0x080483f5 <+5>: mov ds:0x804a01c,eax
0x080483fa <+10>: mov eax,0x3
0x080483ff <+15>: mov ds:0x804a01e,eax
0x08048404 <+20>: mov eax,ds:0x804a01c
0x08048409 <+25>: mov ebx,DWORD PTR ds:0x804a01e
0x0804840f <+31>: add eax,ebx
0x08048411 <+33>: mov ds:0x804a020,eax
0x08048416 <+38>: mov eax,ds:0x804a020
0x0804841b <+43>: ret
0x0804841c <+44>: xchg ax,ax
0x0804841e <+46>: xchg ax,ax
End of assembler dump.
(gdb) quit
好了,我们都看到反汇编代码了。先来检查一下这里test02的反汇编代码,和我们写的汇编代码是不是一致的:
0x080483f0 <+0>: mov eax,0x2
0x080483f5 <+5>: mov ds:0x804a01c,eax
0x080483fa <+10>: mov eax,0x3
0x080483ff <+15>: mov ds:0x804a01e,eax
0x08048404 <+20>: mov eax,ds:0x804a01c
0x08048409 <+25>: mov ebx,DWORD PTR ds:0x804a01e
0x0804840f <+31>: add eax,ebx
0x08048411 <+33>: mov ds:0x804a020,eax
0x08048416 <+38>: mov eax,ds:0x804a020
0x0804841b <+43>: ret
直接和前面写的汇编进行比对便是,由于格式问题,里面的部分地址和标签已经面目全非,但是我们只要能够辨识出来就行了,不需要全部都搞得明明白白。这是前面的汇编代码:
mov eax, 2
mov [x], eax
mov eax, 3
mov [y], eax
mov eax, [x]
mov ebx, [y]
add eax, ebx
mov [z], eax
mov eax, [z]
ret
数一下行数就知道,是相同的。再仔细看看每一条指令,基本也是差不多的。当然x、y、z这些东西不见了,变成了一些奇奇怪怪的符号,在此暂不深究。
我们再看看C程序的汇编代码:
0x080483ed <+0>: push ebp
0x080483ee <+1>: mov ebp,esp
0x080483f0 <+3>: mov DWORD PTR ds:0x804a024,0x2
0x080483fa <+13>: mov DWORD PTR ds:0x804a028,0x3
0x08048404 <+23>: mov edx,DWORD PTR ds:0x804a024
0x0804840a <+29>: mov eax,ds:0x804a028
0x0804840f <+34>: add eax,edx
0x08048411 <+36>: mov ds:0x804a020,eax
0x08048416 <+41>: mov eax,ds:0x804a020
0x0804841b <+46>: pop ebp
0x0804841c <+47>: ret
这里,先撇开下面几个指令(这几个指令本身是有用的,但是在这个例子里,可以暂时先去掉,具体它们是干啥的,后面说),去掉它们:
push ebp
mov ebp, esp
....
pop ebp
于是C程序反汇编变成了这样子:
0x080483f0 <+3>: mov DWORD PTR ds:0x804a024,0x2
0x080483fa <+13>: mov DWORD PTR ds:0x804a028,0x3
0x08048404 <+23>: mov edx,DWORD PTR ds:0x804a024
0x0804840a <+29>: mov eax,ds:0x804a028
0x0804840f <+34>: add eax,edx
0x08048411 <+36>: mov ds:0x804a020,eax
0x08048416 <+41>: mov eax,ds:0x804a020
0x0804841c <+47>: ret
还是看起来不太明朗,怎么办?我们追踪里面的数字2、3和add指令,把那些稀奇古怪的符号换成我们认识的标签x、y、z再看看:
0x080483f0 <+3>: mov [x],0x2
0x080483fa <+13>: mov [y],0x3
0x08048404 <+23>: mov edx,[x]
0x0804840a <+29>: mov eax,[y]
0x0804840f <+34>: add eax,edx
0x08048411 <+36>: mov [z],eax
0x08048416 <+41>: mov eax,[z]
0x0804841c <+47>: ret
对比前面我们自己写的汇编代码看看呢?是不是基本是八九不离十了?仅仅有两个地方不一样:1. 使用的寄存器顺序不太一样,但是这个无妨;2. 有两条汇编指令,在C编译后的反汇编代码中对应的是一条指令。
这里我们发现了,原来
mov eax, 2
mov [x], eax
可以被精简为一条语句:
mov [x], 2
好的,按照C编译器给我们提供的信息,我们的汇编程序还可以简化成这样:
global main
main:
mov [x], 0x2
mov [y], 0x3
mov eax, [x]
mov ebx, [y]
add eax, ebx
mov [z], eax
mov eax, [z]
ret
section .data
x dw 0
y dw 0
z dw 0
然而,当我们把汇编写成这样自己编译的时候,却出错了,这里并不能完全这么写,得做一些小修改,把前两条指令改成:
mov dword [x], 0x2
mov dword [y], 0x3
这样再编译,就没有问题了。通过研究,我们用汇编写出了和前面的C程序编译后代码等价的汇编程序:
global main
main:
mov dword [x], 0x2
mov dword [y], 0x3
mov eax, [x]
mov ebx, [y]
add eax, ebx
mov [z], eax
mov eax, [z]
ret
section .data
x dw 0
y dw 0
z dw 0
好了,到这里,我们通过nasm、gcc和gdb,将一个简单的C程序,用汇编语言等价地实现出来了。
说一下这一段内容的重点:
其实,学习汇编语言的目的,并非主要是为了今后用汇编语言编程,而是借助于对汇编语言的理解,进一步地去理解高级语言在底层的一些细节,一个C语言的赋值语句,一个C语言的加法表达式,在编译后运行的时候,到底在做些什么。也就是通过汇编认识到计算机中,程序执行的时候到底在做些什么,CPU到底在干什么,借助于此,理解计算机程序在CPU眼里的本质。
后续通过这个,结合各种资料学习汇编语言,将是一个非常不错的选择。在对汇编进行实践和理解的过程中,也能更清楚地知道C语言里的各种写法,到底代表什么含义,加深对C语言的认识。
本节内容涉及的代码和操作就多一些了,当然能够耐心做完是最好的,一天两天不够就三天五天,也是值得的。
文中若有疏漏,欢迎指正。
前面说到过这样几个内容:
对应到C语言的学习过程中,无非就是这样几个内容:
好了,到这里,我们继续接下来的话题,程序中的流程控制。
文中涉及一些汇编代码,建议读者自行编程,通过动手实践来加深对程序的理解。
首先,最简单也最好理解的程序流程,便是从前往后的顺序执行。这个非常简单,还是举出前面的例子:
现在有1000个计算题:
99+10=
32-20=
14+21=
47-9=
87+3=
86-8=
...
需要你一个个地从前往后计算,计算结果需要写在专门的答题卡上。当你每做完一个题,你需要继续做下一个题(这不是废话么)。
那么问题来了,我每次计算完一个题目,回头寻找下一个题目的时候,到底哪一个题是我接下来要计算的呢?
你可能会说:瞄一眼答题卡就知道了呀。这就尴尬了,计算机其实是比较傻的,它可没有“瞄一眼”这样的功能。
那这样的话,如果是自己做1000个题目,为了保证做题的时候每一个动作都不是多余的,有一个比较好的办法,就是强行在脑子里记住刚刚那个题目的位置。一会儿回头的时候,就立马知道该继续做哪个题了。
好了,那对于计算机来说呢?前面说到,你做计算题的时候临时留在脑子里的东西,就对应CPU里寄存器的数据。寄存器就充当了临时记住一些东西的功能。那么,在这里,CPU也是用的这个套路,在内部有一个寄存器,专门用来记录程序执行到哪里了。
前面已经有了一个初步的结论,CPU里有一个寄存器专门存放“程序执行到哪里了”这样一个信息,而且这么做也是说得过去的,那就是:必须有一个东西记录当前程序执行到的位置,否则CPU执行完一条指令之后,就不知道接下来该干什么了。
在x86体系结构的CPU里面,这个执行位置的信息,是保存在叫做eip的寄存器中的。不过很遗憾,这个寄存器比较特殊,无法通过mov指令进行修改,也就是说,这么写mov eip, 0x233是行不通的。
(不要问我为什么,我也不知道,这都是人做出来的东西,支不支持就看人家的心情。反正Intel的CPU做出来就是这个样子的,你可以认为,Intel在做CPU的时候压根就没支持这个功能,他们觉得做了也没什么卵用。虽然你可能觉得有这个功能不是更好么,但是实际上,有时候刻意对功能施加一些限制,可以减少程序员写代码误操作的机会,eip这个东西,很关键)
好了,介绍完eip的作用之后,再说一下细节的东西。在执行一条指令的时候,eip此时代表的是下一条指令的位置,eip里保存的就是下一条指令在内存中的地址。这样,CPU在执行完成一条指令之后,就直接根据eip的值,取出下一条指令,同时还要修改eip,往eip上加一个指令的长度,让它继续指向后一条指令。
有了这样一个过程,CPU就能自动地去从前往后执行每一条指令了。而且,上述过程是在CPU中自动发生的,你写代码的时候根本不需要关心这个东西,只需要按照自己的思路从前往后写就是了。
好了,这一段更多的是讲故事,明白CPU里面有个eip寄存器,它的功能很专一,就是用来表示程序现在执行到哪儿了。说得精确一点,eip一直都指向下一个要执行的指令,这一点是由CPU自己保证的。总之,只要CPU没坏,它就能给你保证eip的精确。
前面说了eip能记住程序执行的位置,那么CPU就能顺溜溜地一路走下去了。然而,世界并不是这么美好。因为:
if( a < 1 ){
// some code ...
} else if( a >= 10 ) {
// yi xie dai ma ...
}
实际上有时候我们需要程序有一定的流程控制能力。就是有时候它不是老老实实按照顺序来执行的,中间可能会跳过一些代码,比如上述C代码中的a的值为100的时候。
那么这时候怎么搞呢?照这样说,程序就得具备“修改eip”的能力了,可是前面说了,mov指令不顶用啊?
放心,那帮做CPU的人没那么傻,他们早就想好了怎么办了。他们在设计CPU的时候是这么考虑的:
首先,我们需要更改eip来实现程序突然跳转的效果,进而灵活地对程序的流程进行控制。这里不得不祭出一套新的指令了:跳转指令。
不说了,铺垫也都差不多了,还是直接上代码,直观体验一把,然后再扯别的。先来一份正常的代码:
global main
main:
mov eax, 1
mov ebx, 2
add eax, ebx
ret
如果前面好好学习的话,对这个一定不陌生。还是大致解释一下吧:
eax = 1
ebx = 2
eax = eax + ebx
所以,按照正常逻辑理解,最后eax为3,整个程序退出时会返回3。
好的,到这里,我们来引入新的指令,通过前后对比的变化,来理解新的指令的作用:
global main
main:
mov eax, 1
mov ebx, 2
jmp gun_kai
add eax, ebx
gun_kai:
ret
这段代码相比前面的代码,多了两行:
...
jmp gun_kai
...
gun_kai:
...
好了,这段代码其实没什么功能,存粹是为了演示,运行这个代码,得到的返回结果为1。
好了,最后的结果告诉我们,中间的那一条指令:
add eax, ebx
根本就没有执行,所以最后eax的值就是1,整个程序的返回值就是1。
好了,这里也没什么需要解释的,动手做,稍微对比分析一下就能够知道结论了。程序中出现了一条新的指令jmp,这是一个跳转指令,不解释。这里直接用一个等价的C语言来说明上述功能吧:
int main() {
int a = 1;
int b = 2;
goto gun_kai;
a = a + b;
gun_kai:
return a;
}
实际上,C语言中的goto语句,在编译后就是一条jmp指令。它的功能就是直接跳转到某个地方,你可以往前跳转也可以往后跳转,跳转的目标就是jmp后面的标签,这个标签在经过编译之后,会被处理成一个地址,实际上就是在往某个地址处跳转,而jmp在CPU内部发生的作用就是修改eip,让它突然变成另外一个值,然后CPU就乖乖地跳转过去执行别的地方的代码了。
不对啊,这跳转指令能用来干啥?反正代码都直接被跳过去了,那我编程的时候干脆直接不写那几条指令不就得了么?使用跳转指令是不是有种脱了裤子放屁的感觉?
并不是,继续。
前面说到了跳转,但是仿佛没卵用的样子。接下来我们说这样一个C语言程序:
int main() {
int a = 50;
if( a > 10 ) {
a = a - 10;
}
return a;
}
这个程序,最后的返回值是40,这没什么好解释的。那对应的汇编程序呢?其实也非常简单,先直接给出代码再分析:
global main
main:
mov eax, 50
cmp eax, 10 ; 对eax和10进行比较
jle xiaoyu_dengyu_shi ; 小于或等于的时候跳转
sub eax, 10
xiaoyu_dengyu_shi:
ret
这段汇编代码很关键的地方就在于这两条陌生的指令:
cmp eax, 10 ; 对eax和10进行比较
jle xiaoyu_dengyu_shi ; 小于或等于的时候跳转
先细细解释一下:
到这里,至少上面这个程序,每一条指令都是很清楚的。只是你关心的是下面的问题:
凉拌炒鸡蛋。
别急,先说套路。上面的C语言代码是这样的:
if ( a > 10 ) {
a = a - 10;
}
这是表示:“比较a和10,a大于10的时候,进入if块中执行减法”
而汇编代码:
cmp eax, 10
jle xiaoyu_dengyu_shi
sub eax, 10
xiaoyu_dengyu_shi:
表示的是:“比较eax和10,eax小于等于10的时候,跳过中间的减法”
注意这里最关键的两个表述:
C语言和汇编语言中的条件判断,其组织的思路是刚好相反的。这就在编程的时候带来一些思考上的困难,不过这都还是小事情,实在困难你可以先画出流程图,然后对流程图进行改造,就可以了。
有了上面if的套路,接下来趁热打铁,再做一个练习:
int main() {
int x = 1;
if ( x > 100 ) {
x = x - 20;
}
x = x + 1;
return x;
}
好了,这里按照前面的思路,在汇编语言里面,关键就是下面几点:
按照前面的代码,稍作类比,很容易地就能写出下面的代码来:
global main
main:
mov eax, 1
cmp eax, 100
jle xiao_deng_yu_100
sub eax, 20
xiao_deng_yu_100:
add eax, 1
ret
把程序结合着前面的C代码进行对比,参考前面说的if在汇编里组织的套路,这个程序就很容易理解了。你还可以尝试把
mov eax, 1
更改为:
mov eax, 110
试试程序的执行逻辑是不是发生了变化?
前面说到了if在汇编中的组织方式,接下来,问题就更加复杂了:
凉拌炒鸡蛋。
前面实际上只提到了两个流程控制相关的指令:
以及一个比较指令:
专门用来对两个操作数进行比较。
先从这里入手,总结套路。首先,这两条跳转指令是人想出来的,所以,你很容易想到,仅仅是这两条跳转指令好像还不够。其实,人家做CPU的人早也就想到了。所以,还有这样一些跳转指令:
ja 大于时跳转
jae 大于等于
jb 小于
jbe 小于等于
je 相等
jna 不大于
jnae 不大于或者等于
jnb 不小于
jnbe 不小于或等于
jne 不等于
jg 大于(有符号)
jge 大于等于(有符号)
jl 小于(有符号)
jle 小于等于(有符号)
jng 不大于(有符号)
jnge 不大于等于(有符号)
jnl 不小于
jnle 不小于等于
jns 无符号
jnz 非零
js 如果带符号
jz 如果为零
好了,这就是一些条件跳转指令,将它们配合着前面的cmp指令一起使用,就能够达到if语句的效果。
What?这该不会都得记住吧?其实不用,这里面是有套路的:
比如j后面是ne,对应的是jne跳转指令,n和e分别对应not和equal,也就是“不相等”,也就是说在比较指令的结果为“不想等”的时候,就会跳转。
好了,这里列出来了j后面的字母所对应的含义。根据这些字母的组合,和上述大概的规则,你就能清楚怎么写出这些跳转指令了。当然,这里有“有符号”和“无符号”之分,后面有机会再扯,读者也可以自行了解。
那么,接下来,就可以写出这样的程序所对应的汇编代码了:
int main() {
int x = 10;
if ( x > 100 ) {
x = x - 20;
}
if( x <= 10 ) {
x = x + 10;
}
x = x + 1;
return 0;
}
这个程序没什么卵用,存粹是为了演示。按照前面的套路,其实写出汇编代码也就不难了:
global main
main:
mov eax, 10
cmp eax, 100
jle lower_or_equal_100
sub eax, 20
lower_or_equal_100:
cmp eax, 10
jg greater_10
add eax, 10
greater_10:
add eax, 1
ret
至于更多可能的写法,那就可以慢慢玩了。
这里就不再赘述了,理一下思路:
也就是说,在汇编里面,实际上没有所谓的if或else的说法,只是前面为方便说明,使用了C语言作类比,实际上汇编还可以写得比C语言的判断更加灵活。
事实上,C语言里面的几种常见的if组织结构,都有对应的汇编语言里的套路。说白了,都是套路。
那你怎么才能知道这些套路呢?很简单,用C语言写一个简单的程序,编译后按之前文章所说的内容,使用gdb去反汇编然后就能知道这里面的具体做法了。
下面来尝试下一下:
int main() {
register int grade = 80;
register int level;
if ( grade >= 85 ){
level = 1;
} else if ( grade >= 70 ) {
level = 2;
} else if ( grade >= 60 ) {
level = 3;
} else {
level = 4;
}
return level;
}
(程序中有一个register关键字,是用来限定这个变量在编译后只能用寄存器来进行表示,方便我们进行分析。读者可以根据需要,去掉register关键字后比较一下反汇编代码有何不同。)
这是一个很经典的多分支程序结构。先编译运行,程序返回值为2。
$ gcc -m32 grade.c -o grade
$ ./grade ; echo $?
2
好了,接下来,用gdb进行反汇编:
$ gdb ./grade
(gdb) set disassembly-flavor intel
(gdb) disas main
得到的反汇编代码如下:
Dump of assembler code for function main:
0x080483ed < +0>: push ebp
0x080483ee < +1>: mov ebp,esp
0x080483f0 < +3>: push ebx
0x080483f1 < +4>: mov ebx,0x50
0x080483f6 < +9>: cmp ebx,0x54
0x080483f9 <+12>: jle 0x8048402 <main+21>
0x080483fb <+14>: mov ebx,0x1
0x08048400 <+19>: jmp 0x804841f <main+50>
0x08048402 <+21>: cmp ebx,0x45
0x08048405 <+24>: jle 0x804840e <main+33>
0x08048407 <+26>: mov ebx,0x2
0x0804840c <+31>: jmp 0x804841f <main+50>
0x0804840e <+33>: cmp ebx,0x3b
0x08048411 <+36>: jle 0x804841a <main+45>
0x08048413 <+38>: mov ebx,0x3
0x08048418 <+43>: jmp 0x804841f <main+50>
0x0804841a <+45>: mov ebx,0x4
0x0804841f <+50>: mov eax,ebx
0x08048421 <+52>: pop ebx
0x08048422 <+53>: pop ebp
0x08048423 <+54>: ret
篇幅有限,这里就留给读者练习分析了。其中有几个需要注意的地方:
根据上述反汇编代码,分析出程序的流程图,与C语言程序的代码进行比较。仔细分析,你应该就发现jmp指令有什么用了吧。
到这里,有一个问题出现了,在汇编语言里面实现“先比较,后跳转”的功能时,后面的跳转指令是怎么利用前面的比较结果的呢?
这就涉及到另一个寄存器了。在此之前,先想一下,如果自己在脑子里思考同样的逻辑,是怎么样的?
好了,这里又来了一个“记住”的动作了。CPU里面也有一个专用的寄存器,用来专门“记住”这个cmp指令的比较结果的,而且,不仅是cmp指令,它还会自动记住其它一些指令的结果。这个寄存器就是:
eflags
名为“标志寄存器”,它的作用就是记住一些特殊的CPU状态,比如前一次运算的结果是正还是负、计算过程有没有发生进位、计算结果是不是零等信息,而后续的跳转指令,就是根据eflags寄存器中的状态,来决定是否要进行跳转的。
cmp指令实际上是在对两个操作数进行减法,减法后的一些状态最终就会反映到eflags寄存器中。
这回着重说到了汇编语言中与流程控制相关的内容。其中主要包括:
当然,这里讲述的仅仅是一部分相关的指令,带领读者对这部分内容有一个直观的认识。实际上汇编语言中与流程相关的指令不止这些,读者可自行查阅相关的资料:
本文内容相比之前要更多一些,若想要完全理解,也需要仔细阅读,多思考、多尝试,多验证,也可以参考更多其它方面的资料。
文中若有疏漏之处,欢迎指正。
前面说到在汇编语言中实现类似C语言if-else if-else这样的结构,
实际上,在汇编里面,我们并不关心if了,取而代之的是两种基本的指令:
这两种指令即可组成最基本的分支程序结构,虽然跳转指令非常多,但是我们已经有套路了,怎么跳转都不怕了。当然,在编程环境中仅有分支还不够的,我们知道C语言中除了分支结构之外,还有循环这个最基本也是最常用的形式。正好,这也是本节话题的主角。
文中涉及一些汇编代码,建议读者自行编程,通过动手实践来加深对程序的理解。
上回说到C语言中if这样的结构,在汇编里对应的是怎么回事,实质上,这就是分支结构的程序在汇编里的表现形式。
实际上,循环结构相比分支结构,本质上,没有多少变化,仅仅是比较合跳转指令的组合的方式与顺序有所不同,所以形成了循环。
当然,这个说法可能稍微拗口了一点。说得简单一点,循环的一个关键特点就是:
细细想,好像有道理哦,如果程序每到一个位置就往前跳转,那就是死循环,如果是在这个位置根据条件决定是否要向前跳转,那就是有条件的循环了。
口说无凭,还是先来分析一下一个C语言的while循环:
(Talk is chip, show your code!)
int sum = 0;
int i = 1;
while( i <= 10 ) {
sum = sum + i;
i = i + 1;
}
想必这段程序多数人都非常熟悉了,当年自己第一次学习循环的时候就碰到这个题目,脑子短路了,心里总想着这不就是一个等差数列公式么,题目却强行出现在循环一章的后面,最后结果让人大跌眼睛,这是要我老老实实像SHAB一样去加啊。
跑题了,先大致总结一下这个程序的关键部分到底在干什么:
好了,按照这个逻辑,在C语言中不使用循环怎么实现?其实也非常简单:
int sum = 10;
int i = 1;
_start:
if( i <= 10 ) {
sum = sum + i;
i = i + 1;
goto _start;
}
这还不够,我们还得做一次变形,为什么呢?回想一下前面说的分之程序在汇编里的情况:
if ( a > 10 ) {
// some code
}
上述C代码,暂且成为“正宗C代码”,等价的汇编大致结构如下:
cmp eax, 10
jle out_of_block
; some code
out_of_block:
再等价变换回C语言,这里把这种风格叫做“山寨C代码”,实际上就是这样的:
if( a <= 10 ) goto out_of_block;
// some code
out_of_block:
经过比较,我们可以发现“山寨C代码”和“正宗C代码”之间的一些区别:
相当于是:不满足条件就跳过if中的语句块。
那循环呢?咱们把循环的C等价代码做一次变换,也就是把只含有goto和if的“正宗C代码”变换为“山寨C代码”的形式:
int sum = 10;
int i = 1;
_start:
if( i > 10 ) {
goto _end_of_block;
}
sum = sum + i;
i = i + 1;
goto _start;
_end_of_block:
大致看一下流程,再对比源代码:
int sum = 0;
int i = 1;
while( i <= 10 ) {
sum = sum + i;
i = i + 1;
}
自己在脑子里面模拟一遍,是不是就能发现什么了?这俩货分明就是一个东西,执行的顺序和过程完全就是一样的。
到这里,我们的循环结构,全都被拆散成了最基本的结构,这种结构有一个关键的特点:
到这里,本段就到位了。
前面已经介绍了“如何把一个循环拆解成只有if和goto的结构”,有了这个结构之后,其实要写出汇编就非常容易了。
继续看山寨版的循环:
int sum = 10;
int i = 1;
_start:
if( i > 10 ) {
goto _end_of_block;
}
sum = sum + i;
i = i + 1;
goto _start;
_end_of_block:
其实,稍微仔细一点就能发现,把这玩意儿写成汇编,就是逐行翻译就完事儿了。动手:
global main
main:
mov eax, 0
mov ebx, 1
_start:
cmp ebx, 10
jg _end_of_block
add eax, ebx
add ebx, 1
jmp _start
_end_of_block:
ret
这里面其实有一个套路:
最后,其它语句该干啥干啥。
这?竟然?就?用汇编?写出?循环?来了?
嗯,是的。不需要任何一个新的指令,全都是前面提及过的基本指令,只是套路不一样了而已。
其实这就是一个套路,稍微总结一下就能发现,一个将while循环变换为汇编的过程如下:
那while循环能够搞定了,其它类型的呢?do-while循环、for循环呢?
其实,在C语言中,这三种循环之间都是可以相互变换的,也就是说for循环可以变形成为while循环,while循环也可以变成for循环。举个例子:
int i = 1;
int sum = 0;
for(i = 0; i <= 10; i ++) {
sum = sum + i;
}
int sum = 0;
int i = 1;
while( i <= 10 ) {
sum = sum + i;
i = i + 1;
}
上述两个片段的代码,其实就是等价的,仅仅是形式不同。只是有的循环思路用for循环写出来好看一些,有的思路用while循环写出来好看一些,别的没什么本质区别,经过编译器一倒腾之后,就更没有任何区别了。
在汇编中,分支和循环结构,都是通过两类基本的指令实现的:
只是,分支结构的程序中,所有的跳转目标都是往后,程序一去不复返。而循环结构中,程序会根据条件往前跳转,跳回去执行已经执行过的代码,在绕圈圈,就成循环了。到汇编层面,本质上,没啥区别。
好了,汇编语言中的流程控制,基本就算完事儿了,实际上,在汇编语言中,抓住根本的东西就行了,剩下的就是靠脑子想象了。
文中若有疏漏之处,欢迎指正。
最近忙了一阵,好几天没更了,不好意思,我来晚了。
转入正题,当在汇编中进行函数调用,是一种什么样的体验?
想象你在计算一个非常复杂的数学题,在算到一半的时候,你需要一个数据,而这个数据需要套用一个比较复杂的公式才能算出来,怎么办?
你不得不把手中的事情停下来,先去套公式、代入数值然后…最后,算出结果来了。
这时候你继续开始攻克这个困难题目的剩下部分。
刚刚说的这个过程,可能有点小问题,尤其是对脑子不太好使的人来说。想象你做题目做到一半的时候,记忆力已经有点不好使了,中间突然停下来去算一个复杂的公式,然后回来,诶?我刚刚算到哪了?我刚刚想到哪了?我刚刚算了些什么结果?
在你工作切换的时候,很容易回头来就忘记了刚刚做的部分事情。这时候,为了保证你套完复杂的公式,把结果拿回来继续算题目的时候不会出差错,你需要把刚才计算题目过程中的关键信息写在纸上。
刚刚去套用一个复杂的公式计算某个数据的情景,就类似在计算机里进行函数调用的情景。
程序需要一个结果,这个结果需要通过一个比较复杂的过程进行计算。这时候,编程人员会考虑将这个独立的复杂过程提取为单独的函数。
而在发生函数调用的时候,CPU就像是先暂停当前所做的事情,转去做那个复杂的计算,算完了之后又跳回来继续整个计算。就像你做题的过程中去套了一个公式计算数据一样。
但是在去套用公式之前,你需要做一些准备。首先,默默记下现在这个题目算到哪一步了,一会套完公式回来接着做;默默记下现在计算出来的一些结果,一会可能还会用到;套用公式需要些什么数据,先记下来,代公式的时候直接代入计算,算出来的结果也需要记在脑子里,回头需要使用。
在CPU里面,也需要这几个过程。
第一个,记下自己现在做事情做到哪里了,一会儿套完公式回来接着做,这也就是CPU在进行函数调用时的现场保存操作,CPU也需要记下自己当前执行到哪里了。
默默记下一些在套用公式的时候需要用到的数据,然后去套公式了。这也就是程序中在调用函数的时候进行参数传递的过程。
然后开始执行函数,等函数执行完了,就需要把结果记下来,回去继续刚才要用到数据的那个地方继续算。这也就是函数调用后返回的动作,这个记下的结果就是返回值。
说了那么多故事,那么函数调用要干些啥应该就说清楚了。总结一下大概就这么几个事:
到这里,我们先来一个事例代码,就着代码去发现函数调用中的套路:
global main
eax_plus_1s:
add eax, 1
ret
ebx_plus_1s:
add ebx, 1
ret
main:
mov eax, 0
mov ebx, 0
call eax_plus_1s
call eax_plus_1s
call ebx_plus_1s
add eax, ebx
ret
首先,运行程序,得到结果:3。
上面的代码其实也比较简单,先从主干main这个地方梳理:
上述的两个函数也非常简单,分别就是给eax和ebx加了1。所以,这个程序其实也就是换了个花样给寄存器增加1而已,纯粹演示。
这里出现了一个陌生指令call,这个指令是函数调用专用的指令,从程序的行为上看应该是让程序的执行流程发生跳转。前面说到了跳转指令jmp,这里是call,这两个指令都能让CPU的eip寄存器发生突然变化,然后程序就一下子跳到别的地方去了。但是这两个有区别:
很简单,jmp跳过去了就不知道怎么回来了,而通过call这种方式跳过去后,是可以通过ret指令直接回来的
那这是怎么做到的呢?
其实,在call指令执行的时候,CPU进行跳转之前还要做一个事情,就是把eip保存起来,然后往目标处跳。当遇到ret指令的时候,就把上一次call保存起来的eip恢复回来,我们知道eip直接决定了CPU会执行哪里的代码,当eip恢复的时候,就意味着程序又会到之前的位置了。
一个程序免不了有很多次call,那这些eip的值都是保存到哪里的呢?
有一个地方叫做“栈(stack)”,是程序启动之前,由操作系统指定的一片内存区域,每一次函数调用后的返回地址都存放在栈里面
好了,我们到这里,就明白了函数调用大概是怎么回事了。总结起来就是:
前面说到call指令会先保存eip的值到栈里面,然后就跳转到目标函数中去了。
这都好说,但是,如果是我在函数里面调用了一个函数,在这个函数里面又调用了一个函数,这个eip是怎么保存来保证每一次都能正确的跳回来呢?
好的,这个问题才是关键,这也说到了栈这样一个东西,我们先来设想一些场景,结合实际代码理解一下CPU所对应的栈。
首先,这个栈和数据结构中的栈是不一样的。数据结构中的栈是通过编程语言来形成程序执行逻辑上的栈。而这里的栈,是CPU内硬件实现的栈。当然了,两者在逻辑上都差不多的。
在这里,先回想一下数据结构中基于数组实现的栈。里面最关键的就是需要一个栈顶指针(或者是一个索引、下标),每次放东西入栈,就将指针后移,每一次从栈中取出东西来,就将指针前移。
到这里,我们先从逻辑上分析下CPU在发生函数调用的过程中是如何使用栈的。
假设现在程序处在一个叫做level1的位置,并调用了函数A,在调用的跳转发生之前,会将当前的eip保存起来,这时候,栈里面就是这样的:
---------- <= top
level1
----------
现在,程序处在level2的位置,又调用了函数B,同样,也会保存这次的eip进去:
---------- <= top
level2
----------
level1
----------
再来,程序这次处在level3,调用了C函数,这时候,整个栈就是这样的:
---------- <= top
level3
----------
level2
----------
level1
----------
好了,这下程序执行到了ret,会发生什么事,是不是就回到level3了?在level3中再次执行ret,是不是就回到level2了?以此类推,最终,程序就能做到一层层的函数调用和返回了。
在实际的CPU中,上述的栈顶top也是由一个寄存器来记录的,这个寄存器叫做esp(stack pointer),每次执行call指令的时候。
这里还有一个小细节,在x86的环境下,栈是朝着低地址的方向伸长的。什么意思呢?每一次有东西入栈,那么栈顶指针就会递减一个单位,每一次出栈,栈顶指针就会相应地增加一个单位(和数据结构中一般的做法是相反的)。至于为什么会这样,我也不知道。
eip在入栈的时候,大致就相当于执行了这样一些指令:
sub esp, 4
mov dword ptr[esp], eip
翻译为C语言就是(假如esp是一个void*类型的指针):
esp = (void*)( ((unsigned int)esp) - 4 )
*( (unsigned int*) esp ) = (unsigned int) eip
也就是esp先移动,然后再把eip的值写入到esp指向的内存中。那么,ret执行的时候该干什么,也就非常的清楚了吧。无非就是上述过程的逆过程。
同时,eip寄存器的长度为32位,即4字节,所以每一次入栈出栈的单位大小都是4字节。
没有代码,说个锤子。先来一个简单的程序:
global main
eax_plus_1s:
add eax, 1
ret
main:
mov eax, 0
call eax_plus_1s
ret
这个程序中只有一个函数调用,但不影响我们分析。先编译,得到一个可执行文件,这里先起名为plsone。
然后载入gdb进行调试,进行反汇编:
$ gdb ./plsone
(gdb) disas main
Dump of assembler code for function main:
0x080483f4 <+0>: mov $0x0,%eax
0x080483f9 <+5>: call 0x80483f0 <eax_plus_1s>
0x080483fe <+10>: ret
0x080483ff <+11>: nop
End of assembler dump.
好了,找到反汇编中<+5>所在那一行,对应着的指令是call 0x80483f0,这个指令的地址为:0x080483f9(不同的环境有所不同,根据实际情况来)。按照套路,在这个call指令处打下一个断点,然后运行程序。
(gdb) b *0x080483f9
Breakpoint 1 at 0x80483f9
(gdb) run
Starting program: /home/vagrant/code/asm/07/plsone
Breakpoint 1, 0x080483f9 in main ()
(gdb)
好了,程序执行到断点处,停下来了。再来看反汇编,这次有一个小箭头指向当前的断点了:
(gdb) disas main
Dump of assembler code for function main:
0x080483f4 <+0>: mov $0x0,%eax
=> 0x080483f9 <+5>: call 0x80483f0 <eax_plus_1s>
0x080483fe <+10>: ret
0x080483ff <+11>: nop
End of assembler dump.
接下来,做这样一个事情,看看现在eip的值是多少:
(gdb) info register eip
eip 0x80483f9 0x80483f9 <main+5>
正好指向这个函数调用指令。这里的call指令还没执行,现在的CPU处在上一条指令刚执行完毕的状态。前面说过,CPU中的eip总是指向下一条会执行的指令。在这里,珍惜机会,我们把想看的东西全都看个遍吧:
(gdb) info register esp
esp 0xffffd6ec 0xffffd6ec
(gdb) p/x *(unsigned int*)$esp
$1 = 0xf7e40ad3
该看的都看过了,让程序走吧,让它先执行完了call指令,我们再回头看看什么情况:
(gdb) stepi
0x080483f0 in eax_plus_1s ()
根据提示,程序现在已经执行到函数里面去了。可以直接反汇编看看:
(gdb) disas
Dump of assembler code for function eax_plus_1s:
=> 0x080483f0 <+0>: add $0x1,%eax
0x080483f3 <+3>: ret
End of assembler dump.
现在正等着执行那条加法指令呢。别急,现在函数调用已经发生了,再来看看上面我们看过的一些东西:
(gdb) info register esp
esp 0xffffd6e8 0xffffd6e8
看到了,上次查看esp的时候是0xffffd6ec,进入函数后的esp值是0xffffd6e8。少了个4。
实际上这就是eip被保存到栈里去了,CPU的栈的伸长方向是朝着低地址一侧的,所以每次入栈,esp都会减少一个单位,也就是4。
(gdb) p/x *(unsigned int*)$esp
$2 = 0x80483fe
这次,我们看看栈顶到底是个什么东西,打印出来0x80483fe这么一个玩意儿,这是蛤玩意儿?别急,回头看看main函数的反汇编:
(gdb) disas main
Dump of assembler code for function main:
0x080483f4 <+0>: mov $0x0,%eax
0x080483f9 <+5>: call 0x80483f0 <eax_plus_1s>
0x080483fe <+10>: ret
0x080483ff <+11>: nop
End of assembler dump.
在里面找找0x80483fe呢?刚好在<+10>所在的那一行。这不就是函数调用指令处的后一条指令吗?
对的,也就是说,一会函数返回的时候,就会到<+10>这个地方来。也就是在执行了eax_plus_1s函数里的ret之后。
是不是和前面描述的过程一模一样?
好了,到这里,探究汇编中的函数调用的过程和方法基本就有了,读者可以根据需要自行编写更加奇怪的代码,结合gdb,来探究更多你自己所好奇的东西。
附加一个代码,自己玩耍试试(在自己的环境中玩耍哦):
global main
hahaha:
call hehehe
ret
hehehe:
call hahaha
ret
main:
call hahaha
ret
这回,我们说到这样一些东西:
函数调用最基本的”跳转“和”返回“就这么回事了,下回咱们继续分析”函数调用中的参数传递、返回值和状态“相关的问题。
文中若有疏漏或是不当之处,欢迎指正。
上回说道,x86汇编中专门提供了两个指令call和ret,用于实现函数调用的效果。实际上函数调用就是程序跳转,只是在跳转之前,CPU会保存当前所在的位置(即返回地址),当函数返回时,又可以从调用的位置恢复。返回地址保存在一个叫做“堆栈”的地方,堆栈中可以保存很多个返回地址,同时借助于堆栈的进出逻辑,还能实现函数嵌套、递归等效果。
同时前面还简单地提到了函数调用过程中的参数和返回值的传递过程。实际上,在汇编语言中,函数调用的参数和返回值均可以通过寄存器来传送,只要函数内外相互配合,就可以精确地进行参数和返回值传递。
到这里,看起来好像函数调用的基本要素都有了,但实际上还是有一些问题的。比如说递归调用这样的场景。通过对递归的研究,你也就能明白前面说到的函数调用机制存在什么样致命的问题。
好了,先说下,这部分内容,很关键。
举个例子,通过递归调用来计算斐波那契数列中的某一项,用高级语言编写已经非常容易:
int fibo(int n) {
if(n == 1 || n == 2) {
return 1;
}
return fibo(n - 1) + fibo(n - 2);
}
我们来进行一波改造,改造成接近汇编的形式:
int fibo(int n) {
if(n == 1) {
return 1;
}
if(n == 2) {
return 1;
}
int x = n - 1;
int y = n - 2;
int a = fibo(x);
int b = fibo(y);
int c = a + b;
return c;
}
拆分成这样之后,就能够比较方便地和汇编对应起来了,再改造一下,把变量名全都换成寄存器名,就能够看得更清楚了(先约定eax寄存器作为函数的第一个参数,通过eax也用来传递返回值):
int fibo(int eax) {
int ebx, ecx;
if(eax == 1) {
return eax;
}
if(eax == 2) {
eax = 1;
return eax;
}
int edx = eax;
eax = edx - 1;
eax = fibo(eax);
ebx = eax;
eax = edx - 2;
eax = fibo(eax);
ecx = eax;
eax = ebx + ecx;
return eax;
}
因为eax会被用作参数和返回值,所以进入函数后就需要将eax保存到别的寄存器,一会需要的时候才能够更方便地使用。
看起来,这里的fibo函数已经比较完美了,这个函数在C语言下是能够正常运行的。接下来把它翻译成汇编:
fibo:
cmp eax, 1
je _get_out
cmp eax, 2
je _get_out
mov edx, eax
sub eax, 1
call fibo
mov ebx, eax
mov eax, edx
sub eax, 2
call fibo
mov ecx, eax
mov eax, ebx
add eax, ecx
ret
_get_out:
mov eax, 1
ret
然而,当你使用这个C语言代码翻译出来的汇编的时候,却发现结果怎么都不对了。
那么,问题出在哪里呢?
问题就出在从C语言翻译到汇编的过程中。
在C函数中,虽然我们把各个变量名换成寄存器名,把复杂的语句拆分成简单语句,最后就能够和汇编语句等同起来,但是,在将C代码翻译到汇编的过程中,出现了不等价的变换。其中,变量的作用域便是引起不等价的原因之一。这个C代码:
int fibo(int eax) {
int ebx, ecx;
if(eax == 1) {
return eax;
}
if(eax == 2) {
eax = 1;
return eax;
}
int edx = eax;
eax = edx - 1;
eax = fibo(eax);
ebx = eax;
eax = edx - 2;
eax = fibo(eax);
ecx = eax;
eax = ebx + ecx;
return eax;
}
本身是没有任何问题的。但是,翻译后的汇编就有问题了,实际上上述汇编语言等价为这样的C代码:
int ebx, ecx, edx;
void fibo() {
if(eax == 1) {
eax = 1;
return;
}
if(eax == 2) {
eax = 1;
return;
}
edx = eax;
eax = edx - 1;
eax = fibo(eax);
ebx = eax;
eax = edx - 2;
eax = fibo(eax);
ecx = eax;
eax = ebx + ecx;
}
原因很简单,CPU中的寄存器是全局可见的。所以使用寄存器,实际上就是在使用一个像全局变量一样的东西。
那么,到这里,通过这个例子,你应该能够发现问题了,现有的做法,无法实现递归或者嵌套的结构。
实际上,要实现递归,那么就需要函数的状态是局部可见的,只能在当前这一层函数内访问。递归中会出现层层调用自己的情况,每一层之间的状态都应当保证局部性,不能相互影响。
在C语言的环境下,函数内的局部变量,抽象来看,实际上就是函数执行时的局部状态。在汇编环境下,寄存器是全局可见的,不能用于充当局部变量。
那怎么办呢?
前面说到,堆栈是用来保存函数调用后的返回地址。其实在这里,函数的返回地址,其实就是当前这一层函数的一个状态,这个状态对应的是这一层函数当前执行到哪儿了。
借鉴call指令保存返回地址的思路,如果,在每一层函数中都将当前比较关键的寄存器保存到堆栈中,然后才去调用下一层函数,并且,下层的函数返回的时候,再将寄存器从堆栈中恢复出来,这样也就能够保证下层的函数不会破坏掉上层函数的状了。
也就是,当下要解决这样一个问题:被调用函数在使用一些寄存器的时候,不能影响到调用者所使用的寄存器值,否则函数之间就很难配合好了,也很容易乱套。
实际上,CPU的设计者们已经考虑过这个问题了,所以还专门提供了对应的指令来干这个事。入栈与出栈分别是两个指令:
push eax ; 将eax的值保存到堆栈中去
pop ebx ; 将堆栈顶的值取出并存放到ebx中
有了这两个玩意儿,递归调用这个问题就可以解决了。注意了,这里发生了入栈和出栈的情况,那么,进行栈操作的时候对应的栈顶指针也会发生相应的移动,这里也一样。
先来试一试堆栈的使用,我就不废话了,举个例子,一个通过循环来计算1+2+3+4+5+6+7+…+n的函数(这里还是约定eax为第一个参数,同时eax也是返回值,暂不考虑参数不合法的情况),直接上代码:
sum_one_to_n:
mov ebx, 0
_go_on:
cmp eax, 0
je _get_out:
add ebx, eax
sub eax, 1
jmp _go_on
_get_out:
mov eax, ebx
ret
你可以发现,在这个函数中,不可避免地需要使用到eax之外的寄存器。但是有一个很致命的问题,调用方或者更上层的函数如果使用了ebx寄存器,这里又拿来用,最终,这个sum_one_to_n不小心把上层函数的状态给改了,最后结果和前面的递归例子差不多,总之不是什么好结果。
那么,这里就需要在使用ebx之前,先把ebx保存起来,使用完了之后,再把ebx恢复回来,就不会产生上述问题了。好了,接下来就需要调整代码了,只需要加一行push和pop就能完事儿了。像这样:
sum_one_to_n:
push ebx
mov ebx, 0
_go_on:
cmp eax, 0
je _get_out:
add ebx, eax
sub eax, 1
jmp _go_on
_get_out:
mov eax, ebx
pop ebx
ret
在函数的第一行和倒数第二行分别加入了push ebx和pop ebx指令。
通过push ebx,将当前的ebx寄存器保存起来。
通过pop ebx,堆栈中保存的ebx寄存器恢复回来。
当然了,进行push和pop的时候也得稍加小心,破坏了call指令保存到堆栈中的返回地址,也会坏事的。不过好在,函数内的入栈和出栈操作是保持一致的,不会影响到call指令保存的返回地址,也就不会影响到ret指令的正常工作。
那么,我们就已经解决了函数内保存局部状态的问题了,其中的套路之一便是,让函数在使用某个寄存器之前,先把旧的值保存起来,等用完了之后再恢复回去,那么这个函数执行完毕后,所有的寄存器都是干干净净的,不会被函数玷污。
有了push和pop的解决方案,那么前面那个递归的问题也可以解决了。
先来分析下:
fibo:
cmp eax, 1
je _get_out
cmp eax, 2
je _get_out
mov edx, eax
sub eax, 1
call fibo
mov ebx, eax
mov eax, edx
sub eax, 2
call fibo
mov ecx, eax
mov eax, ebx
add eax, ecx
ret
_get_out:
mov eax, 1
ret
这段代码中使用到了除eax之外的寄存器有ebx、ecx、edx三个。为了保证这三个寄存器不会在不同的递归层级串场,我们需要在函数内使用它们之前将其保存起来,等到不用了之后再还原回去(注意入栈和出栈的顺序是需要反过来的),像这样:。
fibo:
global main
fibo:
cmp eax, 1
je _get_out
cmp eax, 2
je _get_out
push ebx
push ecx
push edx
mov edx, eax
sub eax, 1
call fibo
mov ebx, eax
mov eax, edx
sub eax, 2
call fibo
mov ecx, eax
mov eax, ebx
add eax, ecx
pop edx
pop ecx
pop ebx
ret
_get_out:
mov eax, 1
ret
main:
mov eax, 7
call fibo
ret
编译运行一看,第7项的值为13,诶,这下结果可靠了。我们得到了一个汇编语言实现的、通过递归调用来计算斐波那契数列某一项值的函数。
前面扯了这么多,我们说到了这样一些东西:
在C语言中,x86的32位环境的一般情况下,函数的参数并不是通过寄存器来传递的,返回值也得视情况而定。这取决于编译器怎么做。
实际上,一些基本数据类型,以及指针类型的返回值,一般是通过寄存器eax来传递的,也就是和前面写的汇编一个套路。而参数就不是了,C中的参数一般是通过堆栈来传递的,而非寄存器(当然也可以用寄存器,不过需要加一些特殊的说明)。这里准备了一个例子,供大家体会一下C语言中通过堆栈传递参数的感觉:
(在32位环境下编译)
#include <stdio.h>
int sum(int n, int a, ...) {
int s = 0;
int *p = &a;
for(int i = 0; i < n; i ++) {
s += p[i];
}
return s;
}
int main() {
printf("%d\n", sum(5, 1, 2, 3, 4, 5));
return 0;
}
编译运行:
$ gcc -std=c99 -m32 demo.c -o demo
$ ./demo
15
函数的参数是逐个放到堆栈中的,通过第一个参数的地址,可以挨着往后找到后面所有的参数。你还可以尝试把参数附近的内存都瞧一遍,还能找到混杂在堆栈中的返回地址。
若读者想要对C的函数机制一探究竟,可以尝试编写一些简单的程序,进行反汇编,研究整个程序在汇编这个层面,到底在做些什么。
好了,汇编语言的函数相关部分就可以告一段落了。这部分涉及到一个非常重要的东西:堆栈。这个需要读者下来多了解一些相关的资料,尝试反汇编一些有函数调用的C程序,结合相关的资料不断动手搞事情,去实实在在地体会一下堆栈。
文中若有疏漏或不当之处,欢迎指正。
前面扯了一些个汇编语言的内容,想必读者也应该有了大致的了解。笔者比打算写全面的汇编相关的内容,毕竟目前已经有不少相关的资料了。本入门系列的目的就在于:入门。
完成了入门的任务,入门系列就暂告一段落了。在此,先来对前面提及的内容做一些回顾。前面说到的各项内容大概涉及:
我想,学习汇编中比较容易犯难的几个环节,大致也都覆盖到了。并且教程中也提供了可运行的实例,供读者在学习之后用于验证。
我想,很多新手在了解汇编语言的时候,难免会遇到各种蛋疼的问题。大致有这样一些情况:
这无疑是给学习者泼来一盆冷水,本来只有3分的热情被灭掉了余下0.3分。
其实学习汇编语言,和学习C语言就有所不同了。你不用想着以后用汇编进行编程,学习汇编语言的首要目标是理解CPU运行程序的时候到底在干什么,你编写的C程序或者其他什么代码在CPU的眼里到底是个什么玩意儿,你能够通过汇编去分析程序的行为,解释一些高级语言下无法解释的现象,等等,才是学习的目标。
也就是说,学习汇编语言,应该抱着理解的目标去学习,理解透彻便足矣,无需做到能够流利地用汇编进行编程。
即便如此,笔者前面所述的各方面入门内容也仅仅是入门。不同人有不同的学习历程、只是背景,笔者很难保证自己觉得足够的入门教程,能够让每个读者都刚好受用。鉴于此,笔者将列出在了解了入门内容后需要关心的内容。
前面也有提到,学习汇编所需要侧重的是理解,而非熟练地编代码。汇编语言更像是一套理论知识,用于分析和解释程序在足够底层时的行为和现象。
与其说学习汇编,不如说是揭开高级语言的面纱,深入到更底层的地方去了解计算机原理,靠近计算机程序的本质。深入理解底层原理,有助于建立对计算机更系统的更深入的认识,面对一些看似诡异的问题时能心中有数,有方法有思路有理据去分析和解决。
要做到这一点,是需要循序渐进慢慢学习的。读者可在后续关注这样一些内容:
笔者前面所述的入门内容只能保证覆盖了核心内容,并未覆盖到汇编语言相关的方方面面。读者至少要让自己学习汇编知识覆盖面达到足以形成图灵完备的最小集合。
可以从分析高级语言(比如C)去学习汇编语言,如前面所述的反汇编。关注这样一些内容:
这些概念不仅在x86体系下,在ARM或是MIPS体系下也适用。仅仅是在不同环境下有不同的思路,有不同的表现形式,但是,核心的概念都是一致的。
到这里,便是让你对所有计算机系统的认识有一个大统一。上面说到不同CPU平台下都有相当的共性。组成原理便是对所有计算机系统的大统一,不同平台只是在根据自己的目标特点和偏好在对计算机原理进行应用而已。
组成原理会告诉你,计算机在电路这个层面的本质是什么。
有了上述的汇编、组成原理的认识,同时也还应该去了解计算机操作系统。这里所谓的操作系统是指站在专业的角度,讨论操作系统本质上是在做什么事情,解决什么问题。
这些基础知识将会作为今后进一步学习计算机的坚实基础,基于对计算机、对操作系统的理解,计算机中的一切都将不再神秘,其本质都不过如此,基于自己所学去分析、去理解即可。
笔者在学习的过程中,也尝试过总结适合自己的方法,在此也谈一谈自己学习的方式。
虽然微积分揭示了自然现象和数学之间很多本质的东西,但是让一个小学生直接学习的话,难免会困难重重。这里的问题并不在于知识体系不够全面、不够严谨,而在于学习者对微积分没有基本的感性认识,根本不知道是个啥玩意儿(计算期末考试分数的新方法?),即使记住了这些公式和证明,但自身难以去自行演绎,这其实是无效的学习。
所以,借助此例,结合自身经历,笔者认为学习的时候应该先关注下面一些方面:
学习一个陌生的东西,我都会尝试先寻求一个感性的认识,感性认识达到什么程度呢?就是做到自己能够用一些简洁的白话,把自己接下来要学习的东西到底是什么玩意儿,给解释清楚,最好能够做到让不明白的人也能听明白。
比如什么是计算机?你能想到的就是你面对的那一台电脑,里面有丰富的的软件,可以做很多有趣的事情。
笔者所谓的边界,即接下来要学习的这个东西,能做什么,不能做什么。学习之前明白这一点也非常重要,基于此,便能有一个清晰的目标,对学习后会面对的问题也有一定的底。
比如计算机是无法直接驱动一个火箭上天的,但是能够对航天器的行为进行控制。
学习过程中自己会思考,会想出很多问题,但是这些问题并非都能从书中找到答案。而在学习过程中要刻意的去养成对知识、对自己的新想法进行验证的习惯。这里面大致是这样的过程:
这里的动手验证不是说真的得去拿个锤子砸钉子,而是说这里的分析、思考、试验、演绎等过程需要落到实处,而不是看到了某个模糊的说法,就这样糊弄过去了。
学习时尽量去给自己建立一个足以验证自己所学的环境,或者至少保证自己有了问题知道怎么样去验证。这将是支撑持续而有效学习的可能性和动力的重要基础。
比如通过编程、调试这样的手段去面对语言学习时的各种不解、各种矛盾的问题。通过编程看到结果、进行试验,透过调试去分析、简化学习中的稳题。有搞不明白的代码了,就赶紧编个程序来验证,尽量根据自己的想法把程序写出花儿来,看看到底都发生了些什么。
关于汇编的专门介绍到此就告一段落了,后续笔者将会出更多相关话题的内容,不过内容的深度和难度都会有所上升,也可能更加抽象。
文中若有疏漏或不当之处,欢迎指正。
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="utf-8" />
<title>HTML5 时钟</title>
<link href="css/main.css" rel="stylesheet" type="text/css" />
<script src="http://code.jquery.com/jquery-latest.min.js"></script>
<style>
.clocks {
height: 500px;
margin: 25px auto;
position: relative;
width: 500px;
}
</style>
</head>
<body>
<ul>
<li>
<a href="http://www.webo.com/liuwayong">我的微博</a>
</li>
<li>
<a href="http://http://www.cnblogs.com/Wayou/">我的博客</a>
</li>
<li>
<a href="http://wayouliu.duapp.com/">我的小站</a>
</li>
</ul>
<div>
这是div标签
</div>
</body>
<script>
/*
1.直接扩展jquery 添加方法 sayHello();
*/
// $.extend({
// sayHello: function(name) {
// alert('Hello,' + (name ? name : 'Dude'));
// }
// });
// $.sayHello(); //调用
// $.sayHello('Wayou'); //带参调用
/*
2.在全局空间定义myPlugin
*/
// $.fn.myPlugin = function(options) {
// var defaults = {//定义默认参数
// 'color': 'red',
// 'fontSize': '12px'
// };
// var settings = $.extend({}, defaults, options);//合并用户的options 和默认的defaults到插件空间
// return this.css({//返回方法,利于链式操作
// 'color': settings.color,
// 'fontSize': settings.fontSize
// });
// }
// $(function(){
// $('a').myPlugin({
// 'color': '#2C9929',
// 'fontSize': '36px'
// });
// $('div').myPlugin();
// })
/*
3.在全局空间中定义匿名函数,中定义一个插件
在匿名函数前面加一个";"是一个好的习惯
($, window, document, undefined)匿名函数的参数是为了防止其他人全局屏蔽这几个参数
*/
;(function($, window, document, undefined){
//定义Beautifier的构造函数
var Beautifier = function(ele, opt) {
this.$element = ele,
this.defaults = {
'color': 'red',
'fontSize': '12px',
'textDecoration':'none'
},
this.options = $.extend({}, this.defaults, opt)
}
//定义Beautifier的方法
Beautifier.prototype = {
beautify: function() {
return this.$element.css({
'color': this.options.color,
'fontSize': this.options.fontSize,
'textDecoration': this.options.textDecoration
});
}
}
//在插件中使用Beautifier对象
$.fn.myPlugin = function(options) {
//创建Beautifier的实体
var beautifier = new Beautifier(this, options);
//调用其方法
//return beautifier;//用于3.1调用
return beautifier.beautify();//用于3.2调用
}
})(jQuery, window, document);
//3.1调用
// $(function() {
// $('a').myPlugin({
// 'color': '#2C9929',
// 'fontSize': '12px'
// }).beautify();
// })
//3.2调用
$(function(){
$('a').myPlugin({
'color': '#2C9929',
'fontSize': '20px',
'textDecoration': 'underline'
});
});
</script>
</html>
如何获取要更新的元素,是首先要解决的问题。令人欣慰的是,使用JavaScript获取节点的方法有很多种,这里简单做一下总结(以下方法在IE7和Firefox2.0.0.11测试通过):
1. 通过顶层document节点获取:
(1) document.getElementById(elementId):该方法通过节点的ID,可以准确获得需要的元素,是比较简单快捷的方法。如果页面上含有多个相同id的节点,那么只返回第一个节点。
如今,已经出现了如prototype、Mootools等多个JavaScript库,它们提供了更简便的方法:$(id),参数仍然是节点的id。这个方法可以看作是document.getElementById()的另外一种写法,不过$()的功能更为强大,具体用法可以参考它们各自的API文档。
(2)document.getElementsByName(elementName):该方法是通过节点的name获取节点,从名字可以看出,这个方法返回的不是一个节点元素,而是具有同样名称的节点数组。然后,我们可以通过要获取节点的某个属性来循环判断是否为需要的节点。
例如:在HTML中checkbox和radio都是通过相同的name属性值,来标识一个组内的元素。如果我们现在要获取被选中的元素,首先获取改组元素,然后循环判断是节点的checked属性值是否为true即可。
(3)document.getElementsByTagName(tagName):该方法是通过节点的Tag获取节点,同样该方法也是返回一个数组,例如:document.getElementsByTagName('A')将会返回页面上所有超链接节点。在获取节点之前,一般都是知道节点的类型的,所以使用该方法比较简单。但是缺点也是显而易见,那就是返回的数组可能十分庞大,这样就会浪费很多时间。那么,这个方法是不是就没有用处了呢?当然不是,这个方法和上面的两个不同,它不是document节点的专有方法,还可以应用其他的节点,下面将会提到。
2、通过父节点获取:
(1)parentObj.firstChild:如果节点为已知节点(parentObj)的第一个子节点就可以使用这个方法。这个属性是可以递归使用的,也就是支持parentObj.firstChild.firstChild.firstChild...的形式,如此就可以获得更深层次的节点。
(2)parentObj.lastChild:很显然,这个属性是获取已知节点(parentObj)的最后一个子节点。与firstChild一样,它也可以递归使用。
在使用中,如果我们把二者结合起来,那么将会达到更加令人兴奋的效果,即:parentObj.firstChild.lastChild.lastChild...
(3)parentObj.childNodes:获取已知节点的子节点数组,然后可以通过循环或者索引找到需要的节点。
注意:经测试发现,在IE7上获取的是直接子节点的数组,而在Firefox2.0.0.11上获取的是所有子节点即包括子节点的子节点。
(4)parentObj.children:获取已知节点的直接子节点数组。
注意:经测试,在IE7上,和childNodes效果一样,而Firefox2.0.0.11不支持。这也是为什么我要使用和其他方法不同样式的原因。因此不建议使用。
(5)parentObj.getElementsByTagName(tagName):使用方法不再赘述,它返回已知节点的所有子节点中类型为指定值的子节点数组。例如:parentObj.getElementsByTagName('A')返回已知的子节点中的所有超链接。
3、通过临近节点获取:
(1)neighbourNode.previousSibling:获取已知节点(neighbourNode)的前一个节点,这个属性和前面的firstChild、lastChild一样都似乎可以递归使用的。
(2)neighbourNode.nextSibling:获取已知节点(neighbourNode)的下一个节点,同样支持递归。
4、通过子节点获取:
(1)childNode.parentNode:获取已知节点的父节点。
上面提到的方法,只是一些基本的方法,如果使用了Prototype等JavaScript库,可能还获得其他不同的方法,例如通过节点的class获取等等。不过,如果能够灵活运用上面的各种方法,相信应该可以应付大部分的程序
@charset "utf-8";
/*css reset*/
body,nav,dl,dt,dd,p,h1,h2,h3,h4,ul,ol,li,input,button,textarea,footer{margin:0;padding:0}
body{font:16px/1.5 'Microsoft Yahei','Simsun'; color:#333;background:#fff;-webkit-text-size-adjust: none; min-width:320px;}
h1,h2,h3,h4,h5,h6{font-size:100%}
form{display:inline}
ul,ol{list-style:none}
a{text-decoration:none;color:#1a1a1a}
a:hover, a:active, a:focus{color:#1a1a1a;text-decoration: none;}
a:active{color:#1a1a1a;}
img{vertical-align:middle;border:0;-ms-interpolation-mode:bicubic;}
button,input,select,textarea{font-size:100%; vertical-align:middle;outline:none;}
textarea{resize:none}
button,input[type="button"],input[type="reset"],input[type="submit"] {cursor:pointer;-webkit-appearance:button;-moz-appearance:button}
input:focus:-moz-placeholder,input:focus::-webkit-input-placeholder {color:transparent}
button::-moz-focus-inner,input::-moz-focus-inner { padding:0; border:0}
table { border-collapse:collapse; border-spacing:0}
.fl{float:left;}.fr{float:right;}.hide{display:none;}.show{display: block;}
.ellipsis { white-space:nowrap; text-overflow:ellipsis; overflow:hidden}
.break { word-break:break-all; word-wrap:break-word}
header, footer, article, section, nav, menu, hgroup {display: block;clear:all;}
.clear {zoom:1;}
.clear:after {content:'';display:block;clear:both;height:0;}
.main{width: 100%;font-size: 62.5%;}
控制台输出内容 console.log("content");
数据类型:
Number(整数,浮点数,NaN, Infinity)
Boolean
null表示一个"空"的值
undefined表示"未定义"
NaN === NaN; // false
isNaN(NaN); // true
一定要使用静态模式
在文件或者脚本的最上面写上'use strict'
之后定义变量必须使用var关键字,如果忘记写var则会在控制台报ReferenceError错误
创建对象的构造函数,在strict模式下,this.name = name将报错,因为this绑定为undefined,在非strict模式下,this.name = name不报错,因为this绑定为window,于是无意间创建了全局变量name,并且返回undefined,这个结果更糟糕。
字符串可以用 `` 支持多行模式
alert(`多行
字符串
测试`);
字符串常用
s.length;
s.toUpperCase();//大写
s.toLowerCase();//小写
s.indexOf("..");搜索指定字符串出现的位置
s.substring(0, 5); // 从索引0开始到5(不包括5),返回'hello'
s.substring(7); // 从索引7开始到结束,返回'world'
数组常用
arr.length;
arr.indexOf(value);元素 value 的索引位置
arr.slice();//对应string的substring函数
push('A', 'B')向Array的末尾添加若干元素
arr.pop();//删除数组最后的元素并返回它
arr.unshift('A', 'B');//往Array的头部添加若干元素
arr.shift();//删除数组第一个元素并返回
arr.sort();//数组排序,具体在函数部分
arr.reverse();//反转数组
arr.concat(arr2);//连接两个数组
arr.join('-');//用-将数组连接起来成字符串
对象常用
obj.属性key = 属性value;
delete obj.属性key
'属性key' in ojb;//属性key是否在obj中,继承的也算
obj.hasOwnProperty('属性key');只判断属性key是否在当前对象中
for (var i in 可迭代集合)
for...of//可以循环数组,map,set)
for(var x of a)
var a = ['A', 'B', 'C'];
a.forEach(function (element, index, 被迭代对象) {
// element: 指向当前元素的值
// index: 指向当前索引
// 被迭代对象: 被迭代对象本身
alert(element);
});
map--键值存储
m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);//初始化一个map
m.set('Adam', 67);
m.has('Adam');
m.get('Adam');
m.delete('Adam');
set--不重复集合
var s1 = new Set(); // 空Set
var s2 = new Set([1, 2, 3]); // 含1, 2, 3
s.add(4)
s.delete(3);
函数
arguments关键字,收集函数的所有参数到数组(这是隐藏的可以直接使用)
rest关键字定义变量可以 function foo(a,b, ...rest)
除了前两个参数,后面的存在rest数组中
apply
函数名.apply(被绑定对象, 函数参数)//这样使用时在函数体内的this关键字都是指向"被绑定对象"
call -- 与apply区别
apply()把参数打包成Array再传入
call()把参数按顺序传入
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5
map() reduce()
filter(匿名函数)//过滤,根据匿名函数返回Boolean值决定是否保留
排序函数sort(),原则是按照(规定,对于两个元素x和y,如果认为x < y,则返回-1,如果认为x == y,则返回0,如果认为x > y,则返回1,这样,排序算法就不用关心具体的比较过程,而是根据比较结果直接排序。),并且会修改原数组
数字从小到大排序//从大到小
arr.sort(function (x, y) {
if (x < y) {
return -1;//return 1
}
if (x > y) {
return 1;//return -1
}
return 0;
});
//对字符串数组排序(按照ASCII的小到大)
arr.sort(function (s1, s2) {
x1 = s1.toUpperCase();
x2 = s2.toUpperCase();
if (x1 < x2) {
return -1;
}
if (x1 > x2) {
return 1;
}
return 0;
});
闭包
箭头函数(相当于匿名函数)//还是用匿名函数吧
作用域
使用一个全局变量推荐 window.变量
自己写代码推荐使用一个名字空间(就是建立一个对象,自己的变量都当做对象属性操作)
申明一个块级变量用let
申请一个常量const PI = 3.14;//不可修改
生成器function* fib(参数) {}
js引擎遇到yield就会停止并返回yield后值,并保持函数执行的所有状态,下次调用从上次yield的后面接着执行
调用生成器
一
var f = fib(5);
f.next();
f.next();
f.next();
f.next();
f.next();
二
for (var x of fib(5)) {
console.log(x); // 依次输出0, 1, 1, 2, 3
}
js标准对象
typeof 123; // 'number'
typeof NaN; // 'number'
typeof 'str'; // 'string'
typeof true; // 'boolean'
typeof undefined; // 'undefined'
typeof Math.abs; // 'function'
typeof null; // 'object'
typeof []; // 'object'
typeof {}; // 'object'
总结一下,有这么几条规则需要遵守:
不要使用new Number()、new Boolean()、new String()创建包装对象;
用parseInt()或parseFloat()来转换任意类型到number;
用String()来转换任意类型到string,或者直接调用某个对象的toString()方法;
通常不必把任意类型转换为boolean再判断,因为可以直接写if (myVar) {...};
typeof操作符可以判断出number、boolean、string、function和undefined;
判断Array要使用Array.isArray(arr);
判断null请使用myVar === null;
判断某个全局变量是否存在用typeof window.myVar === 'undefined';
函数内部判断某个变量是否存在用typeof myVar === 'undefined'。
null和undefined没有toString()方法
number使用toString()要(123).toString();
JSON
var xiaoming = {
name: '小明',
age: 14,
gender: true,
height: 1.65,
grade: null,
'middle-school': '\"W3C\" Middle School',
skills: ['JavaScript', 'Java', 'Python', 'Lisp']
};
JSON.stringify(xiaoming); // '{"name":"小明","age":14,"gender":true,"height":1.65,"grade":null,"middle-school":"\"W3C\" Middle School","skills":["JavaScript","Java","Python","Lisp"]}'
JSON.stringify(xiaoming, null, ' ');
{
"name": "小明",
"age": 14,
"gender": true,
"height": 1.65,
"grade": null,
"middle-school": "\"W3C\" Middle School",
"skills": [
"JavaScript",
"Java",
"Python",
"Lisp"
]
}
JSON.stringify(xiaoming, ['name', 'skills'], ' ');
{
"name": "小明",
"skills": [
"JavaScript",
"Java",
"Python",
"Lisp"
]
}
JSON.stringify(xiaoming, convert, ' ');//每个键值都会被convert函数处理
function convert(key, value) {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value;
}
在对象中重写toJSON
var xiaoming = {
name: '小明',
age: 14,
gender: true,
height: 1.65,
grade: null,
'middle-school': '\"W3C\" Middle School',
skills: ['JavaScript', 'Java', 'Python', 'Lisp'],
toJSON: function () {
return { // 只输出name和age,并且改变了key:
'Name': this.name,
'Age': this.age
};
}
};
JSON.stringify(xiaoming); // '{"Name":"小明","Age":14}'
反序列化json
JSON.parse('json字符串')
JSON.parse('json字符串', function(key, value){
//pass
});
js使用classs构建对象
class Student extends 父类{
//构造函数
constructor(name) {
super(name); // 记得用super调用父类的构造方法!
this.name = name;
}
hello() {
alert('Hello, ' + this.name + '!');
}
}
/**
* --------------------------------------------------------------------------
* 小安整理的js函数
* --------------------------------------------------------------------------
* @package JavaScript_function
* @author azhw
* @since Version 1.0
*/
/*-------------------------------------
js获取当前url参数的两种方法
-------------------------------------*/
//方法一
function getQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]); return null;
}
//调用
// alert(GetQueryString("参数名1"));
// alert(GetQueryString("参数名2"));
// alert(GetQueryString("参数名3"));
//方法二
function GetRequest() {
var url = location.search; //获取url中"?"符后的字串
var theRequest = new Object();
if (url.indexOf("?") != -1) {
var str = url.substr(1);
strs = str.split("&");
for(var i = 0; i < strs.length; i ++) {
theRequest[strs[i].split("=")[0]]=unescape(strs[i].split("=")[1]);
}
}
return theRequest;
}
//调用
// <Script language="javascript">
// var Request = new Object();
// Request = GetRequest();
// var 参数1,参数2,参数3,参数N;
// 参数1 = Request['参数1'];
// 参数2 = Request['参数2'];
// 参数3 = Request['参数3'];
// 参数N = Request['参数N'];
// </Script>
/*
---------------------------------------
*/
/*-------------------------------------
js获取屏幕大小
-------------------------------------*/
function a(){
document.write(
"屏幕分辨率为:"+screen.width+"*"+screen.height
+"<br />"+
"屏幕可用大小:"+screen.availWidth+"*"+screen.availHeight
+"<br />"+
"网页可见区域宽:"+document.body.clientWidth
+"<br />"+
"网页可见区域高:"+document.body.clientHeight
+"<br />"+
"网页可见区域宽(包括边线的宽):"+document.body.offsetWidth
+"<br />"+
"网页可见区域高(包括边线的宽):"+document.body.offsetHeight
+"<br />"+
"网页正文全文宽:"+document.body.scrollWidth
+"<br />"+
"网页正文全文高:"+document.body.scrollHeight
+"<br />"+
"网页被卷去的高:"+document.body.scrollTop
+"<br />"+
"网页被卷去的左:"+document.body.scrollLeft
+"<br />"+
"网页正文部分上:"+window.screenTop
+"<br />"+
"网页正文部分左:"+window.screenLeft
+"<br />"+
"屏幕分辨率的高:"+window.screen.height
+"<br />"+
"屏幕分辨率的宽:"+window.screen.width
+"<br />"+
"屏幕可用工作区高度:"+window.screen.availHeight
+"<br />"+
"屏幕可用工作区宽度:"+window.screen.availWidth
);
}
/*
------------------------------------------------
*/
/*-------------------------------------
js模拟鼠标除法点击
-------------------------------------*/
document.getElementById("target").onclick();
document.getElementById("target").click();
//btnObj.click()是真正地用程序去点击按钮,触发了按钮的onclick()事件
//btnObj.onclick()只是简单地调用了btnObj的onclick所指向的方法,只是调用方法而已,并未触发事件
/*-------------------------------------
js控制option
-------------------------------------*/
//循环一个一个移除,可控
/*
<select name="mySelect" id="selectID">
<option value=1>1</option>
<option value=2>2</option>
</select>
*/
var theSelect=document.all.mySelect;
for(var i=theSelect.options.length-1;i>=0;i--)
theSelect.options.remove(i);
//删除内容
document.getElementById("selectID").innerHTML = "";
//令长度为0
document.getElementById("selectID").options.length=0;
//删除制定位置的选项,在第一种里用过
document.getElementById("selectID").options.remove(index);
//添加内容
var selectObj=document.getElementById("selectID");
selectObj.options[selectObj.length] = new Option("mytest", "2");
//selectObj.options[index] = new Option("mytest", "2");
/*
---------------------------------------------------------
*/
/*-------------------------------------
js日期格式转换成时间戳
-------------------------------------*/
/**
2 0 1 5 - 0 4 - 0 5 _ 1 0 : 1 0 : 1 0
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
注: 下划线(_) 代表空格
*/
function transdate(endTime){
var date=new Date();
date.setFullYear(endTime.substring(0,4));
date.setMonth(endTime.substring(5,7)-1);
date.setDate(endTime.substring(8,10));
date.setHours(endTime.substring(11,13));
date.setMinutes(endTime.substring(14,16));
date.setSeconds(endTime.substring(17,19));
return Date.parse(date)/1000;
}
/*
------------------------------------------
*/
/*-------------------------------------
JavaScript 获取当前时间戳:
-------------------------------------*/
var timestamp = Date.parse(new Date());
//结果:1280977330000 获取的时间戳是把毫秒改成000显示,
var timestamp = (new Date()).valueOf();
var timestamp=new Date().getTime();
//获取了当前毫秒的时间戳
/*-------------------------------------
js删除字符串最后一个字符
-------------------------------------*/
//字符串:string s = "1,2,3,4,5,"
//目标:删除最后一个 "," ---> "1,2,3,4,5"
var s = "1,2,3,4,5,";
s=s.substring(0,s.length-1);
alert(s);
/*-------------------------------------
js DOM树加载完成后加载或执行
-------------------------------------*/
// 1 === 2
$(function(){
$("#a").click(function(){
//adding your code here
});
});
//2 === 1
$(document).ready(function(){
$("#a").click(function(){
//adding your code here
});
});
/*-------------------------------------
js 页面加载完成后加载或执行
整个页面的document全部加载完成以后执行,
而且要求所有的外部图片和资源全部加载完成,
如果外部资源,例如图片需要很长时间来加载,那么这个js效果就会让用户感觉失效了。
js里的DOM加载完成后不知,待查
-------------------------------------*/
//1.
window.onload = function(){
$("#a").click(function(){
//adding your code here
});
}
//2.
function myfun(){
alert("this window.onload");
}
window.onload=myfun;//不要括号
/*-------------------------------------
js在页面输出日期和星期,时间
-------------------------------------*/
var Today = new Date();
var DateName = new Array("星期天", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六");
var MonthName = new Array("1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月");
document.write("<span class=\"date\">" + Today.getFullYear() + "年" + MonthName[Today.getMonth()] +
Today.getDate() + "日" + " " + DateName[Today.getDay()] + "</span>");
/*-------------------------------------
js控制input[type=text]获取失去焦点
-------------------------------------*/
<input type="text" value="xiaoan" name="nick_name" onfocus="if(this.value == 'xiaoan') this.value = 'xiao'" onblur="if(this.value =='') this.value = 'xiaoa'">
<pre>
onfocus="if(this.value == 'xiaoan') this.value = 'xiao'"
</pre>
<p>当输入框获得焦点时(onfocus) 如果当前值为xiaoan,那么把输入框的值写成xiao</p>
<pre>
onblur="if(this.value =='') this.value = 'xiaoa'"
</pre>
<p>当输入框失去焦点时(onblur) 如果当前值为'',那么把输入框的值写成xiaoa</p>
<p>反正最后达到一个效果让输入框不为空</p>
/*-------------------------------------
改变url参数并可以把不存在的参数添加进url最后
-------------------------------------*/
function changeURLPar(destiny, par, par_value){
var pattern = par+'=([^&]*)';
var replaceText = par+'='+par_value;
if (destiny.match(pattern)){
var tmp = '/\\'+par+'=[^&]*/';
tmp = destiny.replace(eval(tmp), replaceText);
return (tmp);
}
else{
if (destiny.match('[\?]')){
return destiny+'&'+ replaceText;
}
else{
return destiny+'?'+replaceText;
}
}
return destiny+'\n'+par+'\n'+par_value;
}
//destiny是目标字符串,比如是http://www.huistd.com/?id=3&ttt=3
//par是参数名,par_value是参数要更改的值,调用结果如下:
//changeURLPar(test, 'id', 99); // http://www.huistd.com/?id=99&ttt=3
//添加一个不存在的参数haha
//changeURLPar(test, 'haha', 33); // http://www.huistd.com/?id=99&ttt=3&haha=33
/*
--------------------------------------------------------------------------
*/
/**
* 生成一个自定义范围的随机数
*
* @returns {int}
*/
function GetRandomNum(Min, Max){
var Range = Max - Min;
var Rand = Math.random();
return(Min + Math.round(Rand * Range));
}
/*-------------------------------------
js获取各种节点的方法
-------------------------------------*/
1. 通过顶层document节点获取:
(1) document.getElementById(elementId):该方法通过节点的ID,可以准确获得需要的元素,是比较简单快捷的方法。如果页面上含有多个相同id的节点,那么只返回第一个节点。
如今,已经出现了如prototype、Mootools等多个JavaScript库,它们提供了更简便的方法:$(id),参数仍然是节点的id。这个方法可以看作是document.getElementById()的另外一种写法,不过$()的功能更为强大,具体用法可以参考它们各自的API文档。
(2)document.getElementsByName(elementName):该方法是通过节点的name获取节点,从名字可以看出,这个方法返回的不是一个节点元素,而是具有同样名称的节点数组。然后,我们可以通过要获取节点的某个属性来循环判断是否为需要的节点。
例如:在HTML中checkbox和radio都是通过相同的name属性值,来标识一个组内的元素。如果我们现在要获取被选中的元素,首先获取改组元素,然后循环判断是节点的checked属性值是否为true即可。
(3)document.getElementsByTagName(tagName):该方法是通过节点的Tag获取节点,同样该方法也是返回一个数组,例如:document.getElementsByTagName('A')将会返回页面上所有超链接节点。在获取节点之前,一般都是知道节点的类型的,所以使用该方法比较简单。但是缺点也是显而易见,那就是返回的数组可能十分庞大,这样就会浪费很多时间。那么,这个方法是不是就没有用处了呢?当然不是,这个方法和上面的两个不同,它不是document节点的专有方法,还可以应用其他的节点,下面将会提到。
2、通过父节点获取:
(1)parentObj.firstChild:如果节点为已知节点(parentObj)的第一个子节点就可以使用这个方法。这个属性是可以递归使用的,也就是支持parentObj.firstChild.firstChild.firstChild...的形式,如此就可以获得更深层次的节点。
(2)parentObj.lastChild:很显然,这个属性是获取已知节点(parentObj)的最后一个子节点。与firstChild一样,它也可以递归使用。
在使用中,如果我们把二者结合起来,那么将会达到更加令人兴奋的效果,即:parentObj.firstChild.lastChild.lastChild...
(3)parentObj.childNodes:获取已知节点的子节点数组,然后可以通过循环或者索引找到需要的节点。
注意:经测试发现,在IE7上获取的是直接子节点的数组,而在Firefox2.0.0.11上获取的是所有子节点即包括子节点的子节点。
(4)parentObj.children:获取已知节点的直接子节点数组。
注意:经测试,在IE7上,和childNodes效果一样,而Firefox2.0.0.11不支持。这也是为什么我要使用和其他方法不同样式的原因。因此不建议使用。
(5)parentObj.getElementsByTagName(tagName):使用方法不再赘述,它返回已知节点的所有子节点中类型为指定值的子节点数组。例如:parentObj.getElementsByTagName('A')返回已知的子节点中的所有超链接。
3、通过临近节点获取:
(1)neighbourNode.previousSibling:获取已知节点(neighbourNode)的前一个节点,这个属性和前面的firstChild、lastChild一样都似乎可以递归使用的。
(2)neighbourNode.nextSibling:获取已知节点(neighbourNode)的下一个节点,同样支持递归。
4、通过子节点获取:
(1)childNode.parentNode:获取已知节点的父节点。
/*-------------------------------------
js获取各种时间
-------------------------------------*/
var now = new Date();
now; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)
now.getFullYear(); // 2015, 年份
now.getMonth(); // 5, 月份,注意月份范围是0~11,5表示六月
now.getDate(); // 24, 表示24号
now.getDay(); // 3, 表示星期三
now.getHours(); // 19, 24小时制
now.getMinutes(); // 49, 分钟
now.getSeconds(); // 22, 秒
now.getMilliseconds(); // 875, 毫秒数
now.getTime(); // 1435146562875, 以number形式表示的时间戳
要获取当前时间戳,可以用:
if (Date.now) {
alert(Date.now()); // 老版本IE没有now()方法
} else {
alert(new Date().getTime());
}
function setCookie(name, value, day, path) {
var str = name + "=" + escape(value);
if (day != 0) {
var date = new Date();
var ms = day * 3600 * 1000 * 24;
date.setTime(date.getTime() + ms);
str += ';expires=' + date.toGMTString();
}//undefined
if (typeof (path) != 'undefined') {
str += ';path=' + path
}
document.cookie = str;
}
//获取cookie
function getCookie(c_name)
{
if (document.cookie.length > 0)
{
c_start = document.cookie.indexOf(c_name + "=")
if (c_start != -1)
{
c_start = c_start + c_name.length + 1
c_end = document.cookie.indexOf(";", c_start)
if (c_end == -1)
c_end = document.cookie.length
return unescape(document.cookie.substring(c_start, c_end))
}
}
return ""
}
//这两个函数可以与asp(Server.UrlEncode)、php(urlencode())很好的解码
var test1="http://www.wljcz.com/My first/";
var bb=encodeURIComponent(test1);
var nnow=decodeURIComponent(bb);
document.write(bb+ "<br />");
document.write(nnow);
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<!--分享到-->
<div class="arthdshare">
<!-- Baidu Button BEGIN -->
<div id="bdshare" class="bdshare_t bds_tools get-codes-bdshare">
<span class="bds_more">分享到:</span>
<a class="bds_qzone"></a>
<a class="bds_tsina"></a>
<a class="bds_tqq"></a>
<a class="bds_renren"></a>
<a class="bds_t163"></a>
<a class="shareCount"></a>
</div>
<script type="text/javascript" id="bdshare_js" data="type=tools&uid=6835930" ></script>
<script type="text/javascript" id="bdshell_js"></script>
<script type="text/javascript">
document.getElementById("bdshell_js").src = "http://bdimg.share.baidu.com/static/js/shell_v2.js?cdnversion=" + Math.ceil(new Date()/3600000)
</script>
<!-- Baidu Button END -->
</div>
<!--分享到-->
</body>
</html>
function changeURLPar(destiny, par, par_value)
{
var pattern = par+'=([^&]*)';
var replaceText = par+'='+par_value;
if (destiny.match(pattern))
{
var tmp = '/\\'+par+'=[^&]*/';
tmp = destiny.replace(eval(tmp), replaceText);
return (tmp);
}
else
{
if (destiny.match('[\?]'))
{
return destiny+'&'+ replaceText;
}
else
{
return destiny+'?'+replaceText;
}
}
return destiny+'\n'+par+'\n'+par_value;
}
destiny是目标字符串,比如是http://www.huistd.com/?id=3&ttt=3
par是参数名,par_value是参数要更改的值,调用结果如下:
changeURLPar(test, 'id', 99); // http://www.huistd.com/?id=99&ttt=3
添加一个不存在的参数haha
changeURLPar(test, 'haha', 33); // http://www.huistd.com/?id=99&ttt=3&haha=33
<!DOCTYPE HTML>
<html>
<head>
<title>JavaScript获取浏览器类型与版本</title>
<meta charset="utf-8"/>
<script type="text/javascript">
var Sys = {};
var ua = navigator.userAgent.toLowerCase();
var s;
(s = ua.match(/msie ([\d.]+)/)) ? Sys.ie = s[1] :
(s = ua.match(/firefox\/([\d.]+)/)) ? Sys.firefox = s[1] :
(s = ua.match(/chrome\/([\d.]+)/)) ? Sys.chrome = s[1] :
(s = ua.match(/opera.([\d.]+)/)) ? Sys.opera = s[1] :
(s = ua.match(/version\/([\d.]+).*safari/)) ? Sys.safari = s[1] : 0;
//以下进行测试
if (Sys.ie) document.write('IE: ' + Sys.ie);
if (Sys.firefox) document.write('Firefox: ' + Sys.firefox);
if (Sys.chrome) document.write('Chrome: ' + Sys.chrome);
if (Sys.opera) document.write('Opera: ' + Sys.opera);
if (Sys.safari) document.write('Safari: ' + Sys.safari);
</script>
<script type="text/javascript">
function getBrowserInfo(){
var Sys = {};
var ua = navigator.userAgent.toLowerCase();
var re =/(msie|firefox|chrome|opera|version).*?([\d.]+)/;
var m = ua.match(re);
Sys.browser = m[1].replace(/version/, "'safari");
Sys.ver = m[2];
return Sys;
}
document.write('<hr/>');
//获取当前的浏览器信息
var sys = getBrowserInfo();
//sys.browser得到浏览器的类型,sys.ver得到浏览器的版本
document.write(sys.browser + "的版本是:" + sys.ver);
</script>
</head>
<body>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style type="text/css">
ul li {height: 50px; border: 1px solid red;}
</style>
<script type="text/javascript" src="jquery1.8.2.js"></script>
</head>
<body>
<ul>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
<li>1</li>
</ul>
</body>
<script type="text/javascript">
$(window).load(function(){
;(function($){
var is_do = true;
var range = 50; //距下边界长度/单位px
var maxnum = parseInt(10); //设置加载最多次数-页数
var num = 1; //当前页数
var totalheight = 0;
$(window).scroll(function(){
var srollPos = $(window).scrollTop(); //滚动条距顶部距离(页面超出窗口的高度)
var window_height = $(window).height();//浏览器窗体高度
var document_height = $(document).height();//文档高度
//浏览器窗体高度 + 滚动条距顶部距离 = 文档高度
console.log("滚动条到顶部的垂直高度: "+$(document).scrollTop());
console.log("页面的文档高度 :"+$(document).height());
console.log('浏览器的高度:'+$(window).height());
totalheight = parseFloat(window_height) + parseFloat(srollPos);
if ((document_height-range <= totalheight) && (num < maxnum) && is_do) {
is_do = false;
alert(1);
}
});
})(jQuery);
});
</script>
</html>
<!-- HTML5 -->
<!DOCTYPE html>
<html>
<head>
<title>TouchEvent测试</title>
<meta charset="gbk">
</head>
<body>
<h2>TouchEvent测试</h2>
<br />
<div id="version" style="border:2px solid black;background-color:yellow"></div>
<br />
<br />
<br />
<br />
<br />
<br />
<div id="result" style="border:2px solid red; color:red;">未触发事件!</div>
<div id="test" style="border:2px solid red">
<ul>
<li id="li1">测试条目1</li>
<li id="li2">测试条目2</li>
<li id="li3">测试条目3</li>
<li id="li4">测试条目4</li>
<li id="li5">测试条目5</li>
<li id="li6">测试条目6</li>
<li id="li7">测试条目7</li>
<li id="li8">测试条目8</li>
<li id="li9">测试条目9</li>
<li id="li10">测试条目10</li>
<li id="li11">测试条目11</li>
<li id="li12">测试条目12</li>
<li id="li13">测试条目13</li>
<li id="li14">测试条目14</li>
<li id="li15">测试条目15</li>
<li id="li16">测试条目16</li>
<li id="li17">测试条目17</li>
<li id="li18">测试条目18</li>
<li id="li19">测试条目19</li>
<li id="li20">测试条目20</li>
</ul>
</div>
<script type="text/javascript">
//全局变量,触摸开始位置
var startX = 0, startY = 0;
//touchstart事件
function touchSatrtFunc(evt) {
try
{
//evt.preventDefault(); //阻止触摸时浏览器的缩放、滚动条滚动等
var touch = evt.touches[0]; //获取第一个触点
var x = Number(touch.pageX); //页面触点X坐标
var y = Number(touch.pageY); //页面触点Y坐标
//记录触点初始位置
startX = x;
startY = y;
var text = 'TouchStart事件触发:(' + x + ', ' + y + ')';
document.getElementById("result").innerHTML = text;
}
catch (e) {
alert('touchSatrtFunc:' + e.message);
}
}
//touchmove事件,这个事件无法获取坐标
function touchMoveFunc(evt) {
try
{
//evt.preventDefault(); //阻止触摸时浏览器的缩放、滚动条滚动等
var touch = evt.touches[0]; //获取第一个触点
var x = Number(touch.pageX); //页面触点X坐标
var y = Number(touch.pageY); //页面触点Y坐标
var text = 'TouchMove事件触发:(' + x + ', ' + y + ')';
//判断滑动方向
if (x - startX != 0) {
text += '<br/>左右滑动';
}
if (y - startY != 0) {
text += '<br/>上下滑动';
}
document.getElementById("result").innerHTML = text;
}
catch (e) {
alert('touchMoveFunc:' + e.message);
}
}
//touchend事件
function touchEndFunc(evt) {
try {
//evt.preventDefault(); //阻止触摸时浏览器的缩放、滚动条滚动等
var text = 'TouchEnd事件触发';
document.getElementById("result").innerHTML = text;
}
catch (e) {
alert('touchEndFunc:' + e.message);
}
}
//绑定事件
function bindEvent() {
document.addEventListener('touchstart', touchSatrtFunc, false);
document.addEventListener('touchMoveFunc', touchMoveFunc, false);
document.addEventListener('touchend', touchEndFunc, false);
}
//判断是否支持触摸事件
function isTouchDevice() {
document.getElementById("version").innerHTML = navigator.appVersion;
try {
document.createEvent("TouchEvent");
alert("支持TouchEvent事件!");
bindEvent(); //绑定事件
}
catch (e) {
alert("不支持TouchEvent事件!" + e.message);
}
}
window.onload = isTouchDevice;
</script>
</body>
</html>
js验证身份证号有效性 预览:
单选按钮单击一下选中再次单击取消选中 预览:
模拟js弹出框 预览alert 预览confirm 预览prompt
使用js加载器动态加载外部Javascript文件 无预览
html时钟 预览
jquery-barcode条形码 预览
jQuery1.11.0_20140330.chm 无预览
ajax异步提交表单 无预览
JQUERY字体超出省略号 预览
jqzoom_ev-2.3 商城的商品图片墙 预览 预览 预览 预览 预览 预览
一个管理端模板
标签切换 预览
模拟文件上传 预览
日期插件 预览
图片滚动 预览
网站常用简洁的TAB选项卡 预览
页码样式 预览
event.keycode大全(javascript)
keycode 8 = BackSpace BackSpace
keycode 9 = Tab Tab
keycode 12 = Clear
keycode 13 = Enter
keycode 16 = Shift_L
keycode 17 = Control_L
keycode 18 = Alt_L
keycode 19 = Pause
keycode 20 = Caps_Lock
keycode 27 = Escape Escape
keycode 32 = space space
keycode 33 = Prior
keycode 34 = Next
keycode 35 = End
keycode 36 = Home
keycode 37 = Left
keycode 38 = Up
keycode 39 = Right
keycode 40 = Down
keycode 41 = Select
keycode 42 = Print
keycode 43 = Execute
keycode 45 = Insert
keycode 46 = Delete
keycode 47 = Help
keycode 48 = 0 equal braceright
keycode 49 = 1 exclam onesuperior
keycode 50 = 2 quotedbl twosuperior
keycode 51 = 3 section threesuperior
keycode 52 = 4 dollar
keycode 53 = 5 percent
keycode 54 = 6 ampersand
keycode 55 = 7 slash braceleft
keycode 56 = 8 parenleft bracketleft
keycode 57 = 9 parenright bracketright
keycode 65 = a A
keycode 66 = b B
keycode 67 = c C
keycode 68 = d D
keycode 69 = e E EuroSign
keycode 70 = f F
keycode 71 = g G
keycode 72 = h H
keycode 73 = i I
keycode 74 = j J
keycode 75 = k K
keycode 76 = l L
keycode 77 = m M mu
keycode 78 = n N
keycode 79 = o O
keycode 80 = p P
keycode 81 = q Q at
keycode 82 = r R
keycode 83 = s S
keycode 84 = t T
keycode 85 = u U
keycode 86 = v V
keycode 87 = w W
keycode 88 = x X
keycode 89 = y Y
keycode 90 = z Z
keycode 96 = KP_0 KP_0
keycode 97 = KP_1 KP_1
keycode 98 = KP_2 KP_2
keycode 99 = KP_3 KP_3
keycode 100 = KP_4 KP_4
keycode 101 = KP_5 KP_5
keycode 102 = KP_6 KP_6
keycode 103 = KP_7 KP_7
keycode 104 = KP_8 KP_8
keycode 105 = KP_9 KP_9
keycode 106 = KP_Multiply KP_Multiply
keycode 107 = KP_Add KP_Add
keycode 108 = KP_Separator KP_Separator
keycode 109 = KP_Subtract KP_Subtract
keycode 110 = KP_Decimal KP_Decimal
keycode 111 = KP_Divide KP_Divide
keycode 112 = F1
keycode 113 = F2
keycode 114 = F3
keycode 115 = F4
keycode 116 = F5
keycode 117 = F6
keycode 118 = F7
keycode 119 = F8
keycode 120 = F9
keycode 121 = F10
keycode 122 = F11
keycode 123 = F12
keycode 124 = F13
keycode 125 = F14
keycode 126 = F15
keycode 127 = F16
keycode 128 = F17
keycode 129 = F18
keycode 130 = F19
keycode 131 = F20
keycode 132 = F21
keycode 133 = F22
keycode 134 = F23
keycode 135 = F24
keycode 136 = Num_Lock
keycode 137 = Scroll_Lock
keycode 187 = acute grave
keycode 188 = comma semicolon
keycode 189 = minus underscore
keycode 190 = period colon
keycode 192 = numbersign apostrophe
keycode 210 = plusminus hyphen macron
keycode 211 =
keycode 212 = copyright registered
keycode 213 = guillemotleft guillemotright
keycode 214 = masculine ordfeminine
keycode 215 = ae AE
keycode 216 = cent yen
keycode 217 = questiondown exclamdown
keycode 218 = onequarter onehalf threequarters
keycode 220 = less greater bar
keycode 221 = plus asterisk asciitilde
keycode 227 = multiply division
keycode 228 = acircumflex Acircumflex
keycode 229 = ecircumflex Ecircumflex
keycode 230 = icircumflex Icircumflex
keycode 231 = ocircumflex Ocircumflex
keycode 232 = ucircumflex Ucircumflex
keycode 233 = ntilde Ntilde
keycode 234 = yacute Yacute
keycode 235 = oslash Ooblique
keycode 236 = aring Aring
keycode 237 = ccedilla Ccedilla
keycode 238 = thorn THORN
keycode 239 = eth ETH
keycode 240 = diaeresis cedilla currency
keycode 241 = agrave Agrave atilde Atilde
keycode 242 = egrave Egrave
keycode 243 = igrave Igrave
keycode 244 = ograve Ograve otilde Otilde
keycode 245 = ugrave Ugrave
keycode 246 = adiaeresis Adiaeresis
keycode 247 = ediaeresis Ediaeresis
keycode 248 = idiaeresis Idiaeresis
keycode 249 = odiaeresis Odiaeresis
keycode 250 = udiaeresis Udiaeresis
keycode 251 = ssharp question backslash
keycode 252 = asciicircum degree
keycode 253 = 3 sterling
keycode 254 = Mode_switch
使用event对象的keyCode属性判断输入的键值
eg:if(event.keyCode==13)alert(“enter!”);
键值对应表
A 0X65 U 0X85
B 0X66 V 0X86
C 0X67 W 0X87
D 0X68 X 0X88
E 0X69 Y 0X89
F 0X70 Z 0X90
G 0X71 0 0X48
H 0X72 1 0X49
I 0X73 2 0X50
J 0X74 3 0X51
K 0X75 4 0X52
L 0X76 5 0X53
M 0X77 6 0X54
N 0X78 7 0X55
O 0X79 8 0X56
P 0X80 9 0X57
Q 0X81 ESC 0X1B
R 0X82 CTRL 0X11
S 0X83 SHIFT 0X10
T 0X84 ENTER 0XD
如果要使用组合键,则可以利用event.ctrlKey,event.shiftKey,event .altKey判断是否按下了ctrl键、shift键以及alt键
ES6之前声明变量的方式:var 、 let 、 const
{
var a = 1;
}
console.log(a);
var a = 1;
var a = 2;
console.log(a);
{
let a = 1;
console.log(a);
}
console.log(a); // ReferenceError: a is not defined
let a = 1;
let a = 2;
console.log(a); // SyntaxError: Identifier 'a' has already been declared
### const
1. const 声明的变量必须初始化,并且不能被改变
```javascript
const a;
console.log(a); // SyntaxError: Missing initializer in const declaration
const a = 1;
a = 2; // TypeError: Assignment to constant variable.
console.log(a);
let arr = [1,2,3];
// 传统用法
// let a = arr[0];
// let b = arr[1];
// let c = arr[2];
// 解构用法
let [a,b,c] = arr;
let obj = {
name: 'zhangsan',
age: 18
sex: 'male'
}
// 传统用法
let name = obj.name;
let age = obj.age;
let sex = obj.sex;
// 解构用法
let {name,age,sex} = obj;
console.log(name,age,sex);
let str = "hello.vue";
console.log(str.startsWith("hello"));//true
console.log(str.endsWith(".vue"));//true
console.log(str.includes("e"));//true
console.log(str.includes("hello"));//true
//多行字符串
let ss = `<div>
<span>hello world<span>
</div>`;
console.log(ss);
// 2、字符串插入变量和表达式。变量名写在 ${} 中,${} 中可以放入 JavaScript 表达式。
function fun() {
return "这是一个函数"
}
let info = `我是${abc},今年${age + 10}了, 我想说: ${fun()}`;
console.log(info);
//在ES6以前,我们无法给一个函数参数设置默认值,只能采用变通写法:
function add(a, b) {
// 判断b是否为空,为空就给默认值1
b = b || 1;
return a + b;
}
// 传一个参数
console.log(add(10));
//现在可以这么写:直接给参数写上默认值,没传就会自动使用默认值
function add2(a, b = 1) {
return a + b;
}
console.log(add2(20));
function fun(...values) {
console.log(values.length)
}
fun(1, 2) //2
fun(1, 2, 3, 4) //4
// case1
var print = function (obj) {
console.log(obj);
}
var print = obj => console.log(obj);
print("hello");
// case2
var sum = function (a, b) {
c = a + b;
return a + c;
}
// case2 箭头函数1
var sum2 = (a, b) => a + b;
console.log(sum2(11, 12));
// case2 箭头函数2
var sum3 = (a, b) => {
c = a + b;
return a + c;
}
console.log(sum3(10, 20))
// case3 箭头函数+解构
const person = {
name: "jack",
age: 21,
language: ['java', 'js', 'css']
}
// 传统用法
function hello(person) {
console.log("hello," + person.name)
}
//箭头函数+解构
var hello2 = ({name}) => console.log("hello," +name);
hello2(person); // 直接把包含name属性的一个对象扔进来就行
// case1
const person = {
name: "jack",
age: 21,
language: ['java', 'js', 'css']
}
// [
// "name",
// "age",
// "language"
// ]
console.log(Object.keys(person));
// [
// "jack",
// 21,
// [
// "java",
// "js",
// "css"
// ]
// ]
console.log(Object.values(person));
// [
// [
// "name",
// "jack"
// ],
// [
// "age",
// 21
// ],
// [
// "language",
// [
// "java",
// "js",
// "css"
// ]
// ]
// ]
console.log(Object.entries(person));//[Array(2), Array(2), Array(2)]
// case2
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
//{a:1,b:2,c:3}
Object.assign(target, source1, source2);
// {
// "a": 1,
// "b": 2,
// "c": 3
// }
console.log(target);
// case3 声明对象简写
const age = 23
const name = "张三"
const person1 = { age: age, name: name }
const person2 = { age, name }
console.log(person2);
// case4 对象的函数属性简写
let person3 = {
name: "jack",
// 以前:
eat: function (food) {
console.log(this.name + "在吃" + food);
},
//箭头函数this不能使用,对象.属性
eat2: food => console.log(person3.name + "在吃" + food),
eat3(food) {
console.log(this.name + "在吃" + food);
}
}
person3.eat("香蕉");
person3.eat2("苹果")
person3.eat3("橘子");
// case5 对象拓展运算符
// 1、拷贝对象(深拷贝)
let p1 = { name: "Amy", age: 15 }
let someone = { ...p1 }
console.log(someone) //{name: "Amy", age: 15}
// 2、合并对象
let age1 = { age: 15 }
let name1 = { name: "Amy" }
let p2 = {name:"zhangsan"}
p2 = { ...age1, ...name1 }
console.log(p2)
数组中新增了map和reduce方法。
map():接收一个函数,将原数组中的所有元素用这个函数处理后放入新数组返回。
let arr = ['1', '20', '-5', '3'];
// arr = arr.map((item)=>{
// return item*2
// });
arr = arr.map(item=> item*2);
console.log(arr);
reduce() 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,
[2, 40, -10, 6]
arr.reduce(callback,[initialValue])
/**
1、previousValue (上一次调用回调返回的值,或者是提供的初始值(initialValue))
2、currentValue (数组中当前被处理的元素)
3、index (当前元素在数组中的索引)
4、array (调用 reduce 的数组)
*/
let result = arr.reduce((a,b)=>{
console.log("上一次处理后:"+a);
console.log("当前正在处理:"+b);
return a + b;
},100);
console.log(result)
promise 是一个对象,用于表示一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。promise 对象的构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。resolve 是一个函数,用于将 promise 状态从 pending 转换为 fulfilled,reject 是一个函数,用于将 promise 状态从 pending 转换为 rejected。promise 对象的 then 方法接受两个参数,第一个参数是 promise 成功的回调函数,第二个参数是 promise 失败的回调函数
设定一个功能
传统的写法
$.ajax({
url: "mock/user.json",
success(data) {
console.log("查询用户:", data);
$.ajax({
url: `mock/user_corse_${data.id}.json`,
success(data) {
console.log("查询到课程:", data);
$.ajax({
url: `mock/corse_score_${data.id}.json`,
success(data) {
console.log("查询到分数:", data);
},
error(error) {
console.log("出现异常了:" + error);
}
});
},
error(error) {
console.log("出现异常了:" + error);
}
});
},
error(error) {
console.log("出现异常了:" + error);
}
});
Promise可以封装异步操作 // mock shuju
// mock/user.json
{
"id": 1,
"name": "zhangsan",
"password": "123456"
}
//mock/user_corse_${obj.id}.json
{
"id": 10,
"name": "chinese"
}
// mock/corse_score_${data.id}.json
{
"id": 100,
"score": 90
}
// 第一种
let p = new Promise((resolve, reject) => {
//1、异步操作
$.ajax({
url: "mock/user.json",
success: function (data) {
console.log("查询用户成功:", data)
resolve(data);
},
error: function (err) {
reject(err);
}
});
});
p.then((obj) => {
return new Promise((resolve, reject) => {
$.ajax({
url: `mock/user_corse_${obj.id}.json`,
success: function (data) {
console.log("查询用户课程成功:", data)
resolve(data);
},
error: function (err) {
reject(err)
}
});
})
}).then((data) => {
console.log("上一步的结果", data)
$.ajax({
url: `mock/corse_score_${data.id}.json`,
success: function (data) {
console.log("查询课程得分成功:", data)
},
error: function (err) {
}
});
})
// 方法二
function get(url, data) {
return new Promise((resolve, reject) => {
$.ajax({
url: url,
data: data,
success: function (data) {
resolve(data);
},
error: function (err) {
reject(err)
}
})
});
}
get("mock/user.json")
.then((data) => {
console.log("用户查询成功~~~:", data)
return get(`mock/user_corse_${data.id}.json`);
})
.then((data) => {
console.log("课程查询成功~~~:", data)
return get(`mock/corse_score_${data.id}.json`);
})
.then((data)=>{
console.log("课程成绩查询成功~~~:", data)
})
.catch((err)=>{
console.log("出现异常",err)
});
hello.js
// 默认导出
export default {
sum(a, b) {
return a + b;
}
}
// 具名导出
// export const util = {
// sum(a, b) {
// return a + b;
// }
// }
// export {util}
//`export`不仅可以导出对象,一切JS变量都可以导出。比如:基本类型变量、函数、数组、对象。
user.js
var name = "jack"
var age = 21
function add(a,b){
return a + b;
}
export {name,age,add} // 导出了 变量和函数
main.js
import abc from "./hello.js"
import {name,add} from "./user.js"
abc.sum(1,2);
console.log(name);
add(1,3);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Mybatis Log Helper</title>
<meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" />
<link rel="shortcut icon" href="" />
<script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<style>
#app {
margin-top: 70px;
display: flex;
justify-content: space-evenly;
align-items: center;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
}
</style>
</head>
<body>
<div id="app">
<el-input type="textarea" v-model="pre" placeholder="请复制输入Mybatis打印的日志,须完整Prepareing语句和Parameter语句"
:rows="28" style="width: 600px"></el-input>
<el-button type="success" @click="convert" style="height: 60px; width: 80px;">转换</el-button>
<el-input type="textarea" v-model="res" placeholder="输出结果"
:rows="28" style="width: 600px"></el-input>
</div>
<script type="text/javascript">
const app = new Vue({
el: '#app',
data() {
return {
// 原始str
pre: '',
// 输出结果
res: ''
}
},
methods: {
convert() {
const str = this.pre
if (str.indexOf('Preparing') == -1 || str.indexOf('Parameters') == -1) {
this.$message({
message: '请将Preparing和Parameters语句复制进来',
type: 'error',
center: true
})
}
// str为完整的三行或两行SQL 提取预编译语句
// const prepare = str.substring(str.indexOf('Preparing') + 11, str.indexOf('\n'))
var prepare=""
var params=new Array()
var arr = str.split("\n");
for(const strItem of arr) {
if (strItem.indexOf("Preparing") != -1) {
prepare = strItem.substring(strItem.indexOf('Preparing') + 11)
// console.log("prepare="+prepare)
}
if (strItem.indexOf("Parameters") != -1) {
params.push(strItem.substring(strItem.indexOf('Parameters') + 12))
// console.log("params["+i+"]="+params[i])
}
}
if(params.length > 1) {
var placeholder = prepare.substring(prepare.indexOf('( ?')-1)
console.log(prepare)
for(let i=0; i < params.length-1;i++) {
prepare = prepare+','+placeholder
}
console.log(prepare)
}
var paramStr = params.join(',')
// 获取参数,去空格
paramStr = paramStr.replace(/ /g, '')
// 参数数组
const array = paramStr.split(',')
console.log(array)
// 循环替换占位符,字符串方式替换每次替换第一个
array.map(item => {
youKuoHaoWeiZhi = item.indexOf('(')
console.log(item+":右侧括号"+youKuoHaoWeiZhi)
let newValue = ""
if (youKuoHaoWeiZhi != -1) {
newValue = item.substring(0, item.indexOf('('))
console.log(item+":最新值"+newValue)
// 获取参数类型
const type = item.substring(item.indexOf('(') + 1, item.indexOf(')'))
console.log(item+":参数"+type)
if ('String' === type) {
newValue = "'" + newValue + "'"
}
} else {
newValue = item
}
prepare = prepare .replace('?', newValue)
})
this.res = prepare
}
}
})
// 返回字符串str中的第n字符串reg在str中的索引值index
function index(str, reg, n) {
if (!str || !reg || n <= 0) return -1
// 先求出第一个,再递归n-1
if (n === 1) {
return str.indexOf(reg)
}
// 注意n-1的索引后一定要加1,负责会一直是第一个reg的索引
return str.indexOf(reg, index(str, reg, n - 1) + 1)
}
// 测试index函数
// const str = 'hello world ok'
// const reg = 'o'
// console.log(index(str, reg, 3))
</script>
</body>
</html>
看看人家的就行了:
前端开发的时候,在网页刷新的时候,所有数据都会被清空,这时候就要用到本地存储的技术,前端本地存储的方式有三种,分别是cookie,localstorage和sessionStorage ,这是大家都知道的。本文的主要内容就是针对这三者的存放、获取,区别、应用场景。有需要的朋友可以做一下参考,希望可以帮到大家。
本文首发于blog:obkoro1.com 本文版权归作者所有,转载请注明出处。
很多文档都是说了一大堆,后面用法都没有说,先要学会怎么用,不然后面的都是扯淡,所以这里我先把使用方式弄出来。
保存cookie值:
var dataCookie='110';
document.cookie = 'token' + "=" +dataCookie; 复制代码
获取指定名称的cookie值
function getCookie(name) { //获取指定名称的cookie值
// (^| )name=([^;]*)(;|$),match[0]为与整个正则表达式匹配的字符串,match[i]为正则表达式捕获数组相匹配的数组;
var arr = document.cookie.match(new RegExp("(^| )"+name+"=([^;]*)(;|$)"));
if(arr != null) {
console.log(arr);
return unescape(arr[2]);
}
return null;
}
var cookieData=getCookie('token'); //cookie赋值给变量。复制代码
先贴这两个最基础的方法,文末有个demo里面还有如何设置cookie过期时间,以及删除cookie的、
localStorage和sessionStorage所使用的方法是一样的,下面以sessionStorage为栗子:
var name='sessionData';
var num=120;
sessionStorage.setItem(name,num);//存储数据
sessionStorage.setItem('value2',119);
let dataAll=sessionStorage.valueOf();//获取全部数据
console.log(dataAll,'获取全部数据');
var dataSession=sessionStorage.getItem(name);//获取指定键名数据
var dataSession2=sessionStorage.sessionData;//sessionStorage是js对象,也可以使用key的方式来获取值
console.log(dataSession,dataSession2,'获取指定键名数据');
sessionStorage.removeItem(name); //删除指定键名数据
console.log(dataAll,'获取全部数据1');
sessionStorage.clear();//清空缓存数据:localStorage.clear();
console.log(dataAll,'获取全部数据2'); 复制代码
使用方式,基本上就上面这些,其实也是比较简单的。大家可以把这个copy到自己的编译器里面,或者文末有个demo,可以点开看看。
上面的使用方式说好了,下面就唠唠三者之间的区别,这个问题其实很多大厂面试的时候也都会问到,所以可以注意一下这几个之间的区别。
cookie:可设置失效时间,没有设置的话,默认是关闭浏览器后失效
localStorage:除非被手动清除,否则将会永久保存。
sessionStorage: 仅在当前网页会话下有效,关闭页面或浏览器后就会被清除。
cookie:4KB左右
localStorage和sessionStorage:可以保存5MB的信息。
cookie:每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题
localStorage和sessionStorage:仅在客户端(即浏览器)中保存,不参与和服务器的通信
cookie:需要程序员自己封装,源生的Cookie接口不友好
localStorage和sessionStorage:源生接口可以接受,亦可再次封装来对Object和Array有更好的支持
从安全性来说,因为每次http请求都会携带cookie信息,这样无形中浪费了带宽,所以cookie应该尽可能少的使用,另外cookie还需要指定作用域,不可以跨域调用,限制比较多。但是用来识别用户登录来说,cookie还是比stprage更好用的。其他情况下,可以使用storage,就用storage。
storage在存储数据的大小上面秒杀了cookie,现在基本上很少使用cookie了,因为更大总是更好的,哈哈哈你们懂得。
localStorage和sessionStorage唯一的差别一个是永久保存在浏览器里面,一个是关闭网页就清除了信息。localStorage可以用来夸页面传递参数,sessionStorage用来保存一些临时的数据,防止用户刷新页面之后丢失了一些参数。
localStorage和sessionStorage是html5才应用的新特性,可能有些浏览器并不支持,这里要注意。

cookie的浏览器支持没有找到,可以通过下面这段代码来判断所使用的浏览器是否支持cookie:
if(navigator.cookieEnabled) {
alert("你的浏览器支持cookie功能");//提示浏览器支持cookie
} else {
alert("你的浏览器不支持cookie");//提示浏览器不支持cookie }复制代码

Cookie、localStorage、sessionStorage数据存放处
cookie 使用起来还是需要小心一点,有兴趣的可以看一下这个链接。
把上面的demo代码,上传到github上面了,有需要的小伙伴们,可以看一下。传送门
最后要说的是:不要把什么数据都放在 Cookie、localStorage 和 sessionStorage中,毕竟前端的安全性这么低。只要打开控制台就可以任意的修改 Cookie、localStorage 和 sessionStorage的数据了。涉及到金钱或者其他比较重要的信息,还是要存在后台比较好。
最后:如需转载,请放上原文链接并署名。码字不易,感谢支持!本人写文章本着交流记录的心态,写的不好之处,不撕逼,但是欢迎指点。然后就是希望看完的朋友点个喜欢,也可以关注一下我。
blog网站 and 掘金个人主页
<script>
cookieFn();
strogeFn();
function cookieFn() {
var dataCookie='110';
document.cookie = 'token' + "=" +dataCookie;//直接设置cookie
function getCookie(name) { //获取指定名称的cookie值
// (^| )name=([^;]*)(;|$),match[0]为与整个正则表达式匹配的字符串,match[i]为正则表达式捕获数组相匹配的数组;
var arr = document.cookie.match(new RegExp("(^| )"+name+"=([^;]*)(;|$)"));
if(arr != null) {
console.log(arr,'正则表达式捕获数组相匹配的数组');
return unescape(arr[2]);
}
return null;
}
var cookieData=getCookie('token');
console.log(cookieData,'获取指定名称的cookie值');
function setTime() {
//存储cookie值并且设置cookie过期时间
var date=new Date();
var expiresDays=10;//设置十天过期
date.setTime(date.getTime()+expiresDays*24*3600*1000);
document.cookie="userId=828; expires="+date.toGMTString();
console.log(document.cookie,'存储cookie值并且设置cookie过期时间');
}
setTime();
function delCookie(cookieName1) {
//删除cookie
var date2=new Date();
date2.setTime(date2.getTime()-10001);//把时间设置为过去的时间,会自动删除
document.cookie= cookieName1+"=v; expires="+date2.toGMTString();
console.log(document.cookie,'删除cookie');
}
delCookie('userId');
}
function strogeFn() {
var name='sessionData';
var num=120;
sessionStorage.setItem(name,num);//存储数据
sessionStorage.setItem('value2',119);
let dataAll=sessionStorage.valueOf();//获取全部数据
console.log(dataAll,'获取全部数据');
var dataSession=sessionStorage.getItem(name);//获取指定键名数据
var dataSession2=sessionStorage.sessionData;//sessionStorage是js对象,也可以使用key的方式来获取值
console.log(dataSession,dataSession2,'获取指定键名数据');
sessionStorage.removeItem(name); //删除指定键名数据
console.log(dataAll,'获取全部数据1');
sessionStorage.clear();//清空缓存数据:localStorage.clear();
console.log(dataAll,'获取全部数据2');
}
</script>
学习设计模式重点需要掌握模式的结构(方法结构和类结构也就是UML图)。理解其含义方可应用自如。
与很多软件工程技术一样,模式起源于建筑领域,毕竟与只有几十年历史的软件工程相比,已经拥有几千年沉淀的建筑工程有太多值得学习和借鉴的地方。
那么模式是如何诞生的?让我们先来认识一个人——Christopher Alexander(克里斯托弗.亚历山大),哈佛大学建筑学博士、美国加州大学伯克利分校建筑学教授、加州大学伯克利分校环境结构研究所所长、美国艺术和科学院院士……头衔真多,不过他还有一个“昵称”——模式之父(The father of patterns)。 Christopher Alexander博士及其研究团队用了约20年的时间,对住宅和周边环境进行了大量的调查研究和资料收集工作,发现人们对舒适住宅和城市环境存在一些共同的认同规律,Christopher Alexander在著作《A Pattern Language: Towns, Buildings, Construction》中把这些认同规律归纳为253个模式,对每一个模式(Pattern)都从Context(前提条件)、Theme或Problem(目标问题)、 Solution(解决方案)三个方面进行了描述,并给出了从用户需求分析到建筑环境结构设计直至经典实例的过程模型。
在Christopher Alexander的另一部经典著作《建筑的永恒之道》中,他给出了关于模式的定义:
每个模式都描述了一个在我们的环境中不断出现的问题,然后描述了该问题的解决方案的核心,通过这种方式,我们可以无数次地重用那些已有的成功的解决方案,无须再重复相同的工作。
这个定义可以简单地用一句话表示:
模式是在特定环境下人们解决某类重复出现问题的一套成功或有效的解决方案。
【A pattern is a successful or efficient solution to a recurring problem within a context】
1990年,软件工程界开始关注ChristopherAlexander等在这一住宅、公共建筑与城市规划领域的重大突破。
最早将模式的思想引入软件工程方法学的是1991-1992年以“四人组(Gang of Four,简称GoF,分别是Erich Gamma, Richard Helm, Ralph Johnson和John Vlissides)”自称的四位著名软件工程学者,他们在1994年归纳发表了23种在软件开发中使用频率较高的设计模式,旨在用模式来统一沟通面向对象方法在分析、设计和实现间的鸿沟。
GoF将模式的概念引入软件工程领域,这标志着软件模式的诞生。
软件模式(Software Patterns)是将模式的一般概念应用于软件开发领域,即软件开发的总体指导思路或参照样板。
软件模式并非仅限于设计模式,还包括架构模式、分析模式和过程模式等,实际上,在软件开发生命周期的每一个阶段都存在着一些被认同的模式。
软件模式是在软件开发中某些可重现问题的一些有效解决方法,软件模式的基础结构主要由四部分构成,包括

软件模式与具体的应用领域无关,也就是说无论你从事的是移动应用开发、桌面应用开发、Web应用开发还是嵌入式软件的开发,都可以使用软件模式。
在软件模式中,设计模式是研究最为深入的分支,设计模式用于在特定的条件下为一些重复出现的软件设计问题提供合理的、有效的解决方案,它融合了众多专家的设计经验,已经在成千上万的软件中得以应用。 1995年, GoF将收集和整理好的23种设计模式汇编成Design Patterns: Elements of Reusable Object-Oriented Software【《设计模式:可复用面向对象软件的基础》】一书,该书的出版也标志着设计模式正式成为面向对象(Object Oriented)软件工程的一个重要研究分支。
从1995年至今,无论是在大型API或框架(如JDK、.net Framework等)、轻量级框架(如Struts、Spring、 Hibernate、JUnit等)、还是应用软件的开发中,设计模式都得到了广泛的应用。如果你正在从事面向对象开发或正准备从事面向对象开发,无论你是使用Java、C#、Objective-C、VB.net、Smalltalk等纯面向对象编程语言,还是使用C++、PHP、Delphi、JavaScript等可支持面向对象编程的语言,如果你一点设计模式也不懂,我可以毫不夸张的说:你真的out了。
俗话说:站在别人的肩膀上,我们会看得更远。设计模式的出现可以让我们站在前人的肩膀上,通过一些成熟的设计方案来指导新项目的开发和设计,以便于我们开发出具有更好的灵活性和可扩展性,也更易于复用的软件系统。
设计模式的一般定义如下:
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。
狭义的设计模式是指GoF在《设计模式:可复用面向对象软件的基础》一书中所介绍的23种经典设计模式,不过设计模式并不仅仅只有这23种,随着软件开发技术的发展,越来越多的新模式不断诞生并得以应用。
设计模式一般包含模式名称、问题、目的、解决方案、效果等组成要素,其中关键要素是模式名称、问题、解决方案和效果。
虽然GoF设计模式只有23个,但是它们各具特色,每个模式都为某一个可重复的设计问题提供了一套解决方案。根据它们的用途,设计模式可分为
用于描述如何创建对象 包含5种
用于描述如何实现类或对象的组合 包含7种
用于描述类或对象怎样交互以及怎样分配职责 包含11种
此外,根据某个模式主要是用于处理类之间的关系还是对象之间的关系,设计模式还可以分为类模式和对象模式。我们经常将两种分类方式结合使用,如单例模式是对象创建型模式,模板方法模式是类行为型模式。
值得一提的是,有一个设计模式虽然不属于GoF 23种设计模式,但一般在介绍设计模式时都会对它进行说明,它就是简单工厂模式,也许是太“简单”了,GoF并没有把它写到那本经典著作中,不过现在大部分的设计模式书籍都会对它进行专门的介绍。
表1列出将要介绍的24种设计模式,模式的使用频率来自著名的模式推广和教育网站——http://www.dofactory.net。
| 模式名称 | 使用频率 |
|---|---|
| 单例模式 Singleton Pattern | ★★★★☆ |
| 简单工厂模式 Simple Factory Pattern | ★★★☆☆ |
| 工厂方法模式 Factory Method Pattern | ★★★★☆ |
| 抽象工厂模式 Abstract Factory Pattern | ★★★★★ |
| 原型模式 Prototype Pattern | ★★★☆☆ |
| 建造者模式 Builder Pattern | ★★☆☆☆ |
| 模式名称 | 使用频率 |
|---|---|
| 适配器模式 Adapter Pattern | ★★★★☆ |
| 桥接模式 Bridge Pattern | ★★★☆☆ |
| 组合模式 Composite Pattern | ★★★★☆ |
| 装饰模式 Decorator Pattern | ★★★☆☆ |
| 外观模式 Facade Pattern | ★★★★★ |
| 享元模式 Flyweight Pattern | ★☆☆☆☆ |
| 代理模式 Proxy Pattern | ★★★★☆ |
| 模式名称 | 使用频率 |
|---|---|
| 职责链模式 Pattern | ★★☆☆☆ |
| 命令模式 Pattern | ★★★★☆ |
| 解释器模式 Pattern | ★☆☆☆☆ |
| 迭代器模式 Pattern | ★★★★★ |
| 中介者模式 Pattern | ★★☆☆☆ |
| 备忘录模式 Pattern | ★★☆☆☆ |
| 观察者模式 Pattern | ★★★★★ |
| 状态模式 Pattern | ★★★☆☆ |
| 策略模式 Pattern | ★★★★☆ |
| 模板方法模式 Pattern | ★★★☆☆ |
| 访问者模式 Pattern | ★☆☆☆☆ |
设计模式来源众多专家的经验和智慧,它们是从许多优秀的软件系统中总结出的成功的、能够实现可维护性复用的设计方案,使用这些方案将可以让我们避免做一些重复性的工作,也许我们冥思苦想得到的一个“自以为很了不起”的设计方案其实就是某一个设计模式。在时间就是金钱的今天,设计模式无疑会为有助于我们提高开发和设计效率,但它不保证一定会提高。
设计模式提供了一套通用的设计词汇和一种通用的形式来方便开发人员之间沟通和交流,使得设计方案更加通俗易懂。交流通常很耗时,任何有助于提高交流效率的东西都可以为我们节省不少时间。无论你使用哪种编程语言,做什么类型的项目,甚至你处于一个国际化的开发团队,当面对同一个设计模式时,你和别人的理解并无二异,因为设计模式是跨语言、跨平台、跨应用、跨国界的。
大部分设计模式都兼顾了系统的可重用性和可扩展性,这使得我们可以更好地重用一些已有的设计方案、功能模块甚至一个完整的软件系统,避免我们经常做一些重复的设计、编写一些重复的代码。此外,随着软件规模的日益增大,软件寿命的日益变长,系统的可维护性和可扩展性也越来越重要,许多设计模式将有助于提高系统的灵活性和可扩展性,让我们在不修改或者少修改现有系统的基础上增加、删除或者替换功能模块。如果一点设计模式都不懂,我想要做到这一点恐怕还是很困难的。
合理使用设计模式并对设计模式的使用情况进行文档化,将有助于别人更快地理解系统。如果某一天因为升职或跳槽等原因,别人接手了你的项目,只要他也懂设计模式,我想他应该能够很快理解你的设计思路和实现方案,让你升职无后患之忧,跳槽也心安理得,何乐而不为呢?
最后一点对初学者很重要,学习设计模式将有助于初学者更加深入地理解面向对象思想,让你知道:如何将代码分散在几个不同的类中?为什么要有“接口”?何谓针对抽象编程?何时不应该使用继承?如果不修改源代码增加新功能?同时还让你能够更好地阅读和理解现有类库(如JDK)与其他系统中的源代码,让你早点脱离面向对象编程的“菜鸟期”。
掌握设计模式并不是件很难的事情,关键在于多思考,多实践,不要听到人家说懂几个设计模式就很“牛”,只要用心学习,设计模式也就那么回事,你也可以很“牛”的,一定要有信心。
在学习每一个设计模式时至少应该掌握如下几点:这个设计模式的意图是什么,它要解决一个什么问题,什么时候可以使用它;它是如何解决的,掌握它的结构图,记住它的关键代码;能够想到至少两个它的应用实例,一个生活中的,一个软件中的;这个模式的优缺点是什么,在使用时要注意什么。当你能够回答上述所有问题时,恭喜你,你了解一个设计模式了,至于掌握它,那就在开发中去使用吧,用多了你自然就掌握了。
“如果想体验一下运用模式的感觉,那么最好的方法就是运用它们”。正如在本章最开始所说的,设计模式是“内功心法”,它还是要与“实战招式”相结合才能够相得益彰。学习设计模式的目的在于应用,如果不懂如何使用一个设计模式,而只是学过,能够说出它的用途,绘制它的结构,充其量也只能说你了解这个模式,严格一点说:不会在开发中灵活运用一个模式基本上等于没学。所以一定要做到:少说多做。
千万不要滥用模式,不要试图在一个系统中用上所有的模式,也许有这样的系统,但至少目前我没有碰到过。每个模式都有自己的适用场景,不能为了使用模式而使用模式?【怎么理解,大家自己思考】,滥用模式不如不用模式,因为滥用的结果得不到“艺术品”一样的软件,很有可能是一堆垃圾代码。
如果将设计模式比喻成“三十六计”,那么每一个模式都是一种计策,它为解决某一类问题而诞生,不管这个设计模式的难度如何,使用频率高不高,我建议大家都应该好好学学,多学一个模式也就意味着你多了“一计”,说不定什么时候一不小心就用上了。因此,模式学习之路上要不怕困难,勇于挑战,有的模式虽然难一点,但反复琢磨,反复研读,应该还是能够征服的。
设计模式的“上乘”境界:“手中无模式,心中有模式”。模式使用的最高境界是你已经不知道具体某个设计模式的定义和结构了,但你会灵活自如地选择一种设计方案【其实就是某个设计模式】来解决某个问题,设计模式已经成为你开发技能的一部分,能够手到擒来,“内功”与“招式”已浑然一体,要达到这个境界并不是看完某本书或者开发一两个项目就能够实现的,它需要不断沉淀与积累,所以,对模式的学习不要急于求成。
最后一点来自GoF已故成员、我个人最尊敬和崇拜的软件工程大师之一John Vlissides的著作《设计模式沉思录》(Pattern Hatching Design Patterns Applied):模式从不保证任何东西,它不能保证你一定能够做出可复用的软件,提高你的生产率,更不能保证世界和平。模式并不能替代人来完成软件系统的创造,它们只不过会给那些缺乏经验但却具备才能和创造力的人带来希望。
John Vlissides(1961-2005),GoF成员,斯坦福大学计算机科学博士,原IBM研究员,因患脑瘤于2005年11月24日(感恩节)病故,享年44岁,为纪念他的贡献,ACM SIGPLAN特设立John Vlissides奖。
对于面向对象软件系统的设计而言,在支持可维护性的同时,提高系统的可复用性是一个至关重要的问题,如何同时提高一个软件系统的可维护性和可复用性是面向对象设计需要解决的核心问题之一。
在面向对象设计中,可维护性的复用是以设计原则为基础的。每一个原则都蕴含一些面向对象设计的思想,可以从不同的角度提升一个软件结构的设计水平。
面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在很多设计模式中,它们是从许多设计方案中总结出的指导性原则。
面向对象设计原则也是我们用于评价一个设计模式的使用效果的重要指标之一,在设计模式的学习中,大家经常会看到诸如“XXX模式符合XXX原则”、“XXX模式违反了XXX原则”这样的语句。
最常见的7种面向对象设计原则如下表所示:
| 设计原则名称 | 定 义 | 使用频率 |
|---|---|---|
| 单一职责原则(Single Responsibility Principle, SRP) | 一个类只负责一个功能领域中的相应职责 | ★★★★☆ |
| 开闭原则(Open-Closed Principle, OCP) | 软件实体应对扩展开放,而对修改关闭 | ★★★★★ |
| 里氏代换原则(Liskov Substitution Principle, LSP) | 所有引用基类对象的地方能够透明地使用其子类的对象 | ★★★★★ |
| 依赖倒转原则(Dependence Inversion Principle, DIP) | 抽象不应该依赖于细节,细节应该依赖于抽象 | ★★★★★ |
| 接口隔离原则(Interface Segregation Principle, ISP) | 使用多个专门的接口,而不使用单一的总接口 | ★★☆☆☆ |
| 合成复用原则(Composite Reuse Principle, CRP) | 尽量使用对象组合,而不是继承来达到复用的目的 | ★★★★☆ |
| 迪米特法则(Law of Demeter, LoD) | 一个软件实体应当尽可能少地与其他实体发生相互作用 | ★★★☆☆ |
单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小。单一职责原则定义如下:
单一职责原则(Single Responsibility Principle, SRP):一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
单一职责原则告诉我们:一个类不能太“累”!在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
下面通过一个简单实例来进一步分析单一职责原则:
例如一个CRM(Customer Relationship Management)客户管理系统)系统中客户信息图形统计模块的初始设计方案

CustomerDataChart类中的方法说明如下:
getConnection()方法用于连接数据库,
findCustomers()用于查询所有的客户信息,
createChart()用于创建图表,
displayChart()用于显示图表。
现使用单一职责原则对其进行重构。
CustomerDataChart类承担了太多的职责,既包含与数据库相关的方法,又包含与图表生成和显示相关的方法。
如果在其他类中也需要连接数据库或者使用findCustomers()方法查询客户信息,则难以实现代码的重用。无论是修改数据库连接方式还是修改图表显示方式都需要修改该类,它不止一个引起它变化的原因,违背了单一职责原则。
因此需要对该类进行拆分,使其满足单一职责原则,类CustomerDataChart可拆分为如下三个类:
(1) DBUtil:负责连接数据库,包含数据库连接方法getConnection();
(2) CustomerDAO:负责操作数据库中的Customer表,包含对Customer表的增删改查等方法,如findCustomers();
(3) CustomerDataChart:负责图表的生成和显示,包含方法createChart()和displayChart()。
使用单一职责原则重构后的结构如图2所示:

开闭原则是面向对象的可复用设计的第一块基石,它是最重要的面向对象设计原则。开闭原则由Bertrand Meyer于1988年提出,其定义如下:
开闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
在开闭原则的定义中,软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。
任何软件都需要面临一个很重要的问题,即它们的需求会随时间的推移而发生变化。当软件系统需要面对新的需求时,我们应该尽量保证系统的设计框架是稳定的。
如果一个软件设计符合开闭原则,那么可以非常方便地对系统进行扩展,而且在扩展时无须修改现有代码,使得软件系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。
随着软件规模越来越大,软件寿命越来越长,软件维护成本越来越高,设计满足开闭原则的软件系统也变得越来越重要。
为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。在Java、C#等编程语言中,可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。
在很多面向对象编程语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。
如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。
CRM系统可以显示各种类型的图表,如饼状图和柱状图等,为了支持多种图表显示方式,原始设计方案如图1所示:
在ChartDisplay类的display()方法中存在如下代码片段:
......
if (type.equals("pie")) {
PieChart chart = new PieChart();
chart.display();
}
else if (type.equals("bar")) {
BarChart chart = new BarChart();
chart.display();
}
......
在该代码中,如果需要增加一个新的图表类,如折线图LineChart,则需要修改ChartDisplay类的display()方法的源代码,增加新的判断逻辑,违反了开闭原则。
现对该系统进行重构,使之符合开闭原则。
在本实例中,由于在ChartDisplay类的display()方法中针对每一个图表类编程,因此增加新的图表类不得不修改源代码。可以通过抽象化的方式对系统进行重构,使之增加新的图表类时无须修改源代码,满足开闭原则。具体做法如下:
(1) 增加一个抽象图表类AbstractChart,将各种具体图表类作为其子类;
(2) ChartDisplay类针对抽象图表类进行编程,由客户端来决定使用哪种具体图表。
重构后结构如图2所示:
在图2中,我们引入了抽象图表类AbstractChart,且ChartDisplay针对抽象图表类进行编程,并通过setChart()方法由客户端来设置实例化的具体图表对象,在ChartDisplay的display()方法中调用chart对象的display()方法显示图表。
如果需要增加一种新的图表,如折线图LineChart,只需要将LineChart也作为AbstractChart的子类,在客户端向ChartDisplay中注入一个LineChart对象即可,无须修改现有类库的源代码。
注意:因为xml和properties等格式的配置文件是纯文本文件,可以直接通过VI编辑器或记事本进行编辑,且无须编译,因此在软件开发中,一般不把对配置文件的修改认为是对系统源代码的修改。如果一个系统在扩展时只涉及到修改配置文件,而原有的Java代码或C#代码没有做任何修改,该系统即可认为是一个符合开闭原则的系统。
里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出。
其严格表述如下:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1代换o2时,程序P的行为没有变化,那么类型S是类型T的子类型。这个定义比较拗口且难以理解,因此我们一般使用它的另一个通俗版定义:
里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。
例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。
例如有两个类,一个类为BaseClass,另一个是SubClass类,并且SubClass类是BaseClass类的子类,那么一个方法如果可以接受一个BaseClass类型的基类对象base的话,如:method1(base),那么它必然可以接受一个BaseClass类型的子类对象sub,method1(sub)能够正常运行。反过来的代换不成立,如一个方法method2接受BaseClass类型的子类对象sub为参数:method2(sub),那么一般而言不可以有method2(base),除非是重载方法。
里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
在使用里氏代换原则时需要注意如下几个问题:
子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
我们在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。
Java语言中,在编译阶段,Java编译器会检查一个程序是否符合里氏代换原则,这是一个与实现无关的、纯语法意义上的检查,但Java编译器的检查是有局限的。
在Sunny软件公司开发的CRM系统中,客户(Customer)可以分为VIP客户(VIPCustomer)和普通客户(CommonCustomer)两类,系统需要提供一个发送Email的功能,原始设计方案如图1所示:

在对系统进行进一步分析后发现,无论是普通客户还是VIP客户,发送邮件的过程都是相同的,也就是说两个send()方法中的代码重复,而且在本系统中还将增加新类型的客户。为了让系统具有更好的扩展性,同时减少代码重复,使用里氏代换原则对其进行重构。
在本实例中,可以考虑增加一个新的抽象客户类Customer,而将CommonCustomer和VIPCustomer类作为其子类,邮件发送类EmailSender类针对抽象客户类Customer编程,根据里氏代换原则,能够接受基类对象的地方必然能够接受子类对象,因此将EmailSender中的send()方法的参数类型改为Customer,如果需要增加新类型的客户,只需将其作为Customer类的子类即可。重构后的结构如图2所示:

图2 重构后的结构图
**里氏代换原则是实现开闭原则的重要方式之一。**在本实例中,在传递参数时使用基类对象,除此以外,在定义成员变量、定义局部变量、确定方法返回类型时都可使用里氏代换原则。针对基类编程,在程序运行时再确定具体子类。
扩展: 里氏代换原则以Barbara Liskov(芭芭拉·利斯科夫)教授的姓氏命名。芭芭拉·利斯科夫:美国计算机科学家,2008年图灵奖得主,2004年约翰·冯诺依曼奖得主,美国工程院院士,美国艺术与科学院院士,美国计算机协会会士,麻省理工学院电子电气与计算机科学系教授,美国第一位计算机科学女博士。
如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要实现机制之一,它是系统抽象化的具体实现。
依赖倒转原则是Robert C. Martin在1996年为“C++Reporter”所写的专栏Engineering Notebook的第三篇,后来加入到他在2002年出版的经典著作“Agile Software Development, Principles, Patterns, and Practices”一书中。依赖倒转原则定义如下:
依赖倒转原则(Dependency Inversion Principle, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。
在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。
在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过**依赖注入(DependencyInjection, DI)**的方式注入到其他对象中, 依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。
常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。
构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过Setter方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。
这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。
扩展 软件工程大师Martin Fowler在其文章Inversion of Control Containers and the Dependency Injection pattern中对依赖注入进行了深入的分析,参考链接:http://martinfowler.com/articles/injection.html
下面通过一个简单实例来加深对依赖倒转原则的理解:
Sunny软件公司开发人员在开发某CRM系统时发现:该系统经常需要将存储在TXT或Excel文件中的客户信息转存到数据库中,因此需要进行数据格式转换。
在客户数据操作类中将调用数据格式转换类的方法实现格式转换和数据库插入操作,初始设计方案结构如图1所示:

图1 初始设计方案结构图
在编码实现图1所示结构时,Sunny软件公司开发人员发现该设计方案存在一个非常严重的问题,由于每次转换数据时数据来源不一定相同,因此需要更换数据转换类,如有时候需要将TXTDataConvertor改为ExcelDataConvertor, 此时,需要修改CustomerDAO的源代码,而且在引入并使用新的数据转换类时也不得不修改CustomerDAO的源代码,系统扩展性较差,违反了开闭原则,现需要对该方案进行重构。
在本实例中,由于CustomerDAO针对具体数据转换类编程,因此在增加新的数据转换类或者更换数据转换类时都不得不修改CustomerDAO的源代码。 我们可以通过引入抽象数据转换类解决该问题,在引入抽象数据转换类DataConvertor之后,CustomerDAO针对抽象类DataConvertor编程,而将具体数据转换类名存储在配置文件中,符合依赖倒转原则。 根据里氏代换原则,程序运行时,具体数据转换类对象将替换DataConvertor类型的对象,程序不会出现任何问题。 更换具体数据转换类时无须修改源代码,只需要修改配置文件;如果需要增加新的具体数据转换类,只要将新增数据转换类作为DataConvertor的子类并修改配置文件即可,原有代码无须做任何修改,满足开闭原则。
重构后的结构如图2所示:

图2重构后的结构图
在上述重构过程中,我们使用了开闭原则、里氏代换原则和依赖倒转原则,在大多数情况下,这三个设计原则会同时出现,开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段, 它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不同而已。
扩展
Robert C. Martin(Bob大叔):Object Mentor公司总裁,面向对象设计、模式、UML、敏捷方法学和极限编程领域内的资深顾问。

再上两张Bob大叔的“玉照”,


接口隔离原则定义如下:
接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。 每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。 这里的“接口”往往有两种不同的含义: 一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象; 另外一种是指某种语言具体的“接口”定义,有严格的定义和结构,比如Java语言中的interface。
对于这两种不同的含义,ISP的表达方式以及含义都有所不同:
当把“接口”理解成一个类型所提供的所有方法特征的集合的时候,这就是一种逻辑上的概念,接口的划分将直接带来类型的划分。 可以把接口理解成角色,一个接口只能代表一个角色,每个角色都有它特定的一个接口,此时,这个原则可以叫做“角色隔离原则”。
如果把“接口”理解成狭义的特定语言的接口,那么ISP表达的意思是指接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。 在面向对象编程语言中,实现一个接口就需要实现该接口中定义的所有方法,因此大的总接口使用起来不一定很方便,为了使接口的职责单一,需要将大接口中的方法根据其职责不同分别放在不同的小接口中,以确保每个接口使用起来都较为方便,并都承担某一单一角色。 接口应该尽量细化,同时接口中的方法应该尽量少,每个接口中只包含一个客户端(如子模块或业务逻辑类)所需的方法即可,这种机制也称为“定制服务”,即为不同的客户端提供宽窄不同的接口。
下面通过一个简单实例来加深对接口隔离原则的理解:
Sunny软件公司开发人员针对某CRM系统的客户数据显示模块设计了如图1所示接口,其中方法dataRead()用于从文件中读取数据,方法transformToXML()用于将数据转换成XML格式,方法createChart()用于创建图表,方法displayChart()用于显示图表,方法createReport()用于创建文字报表,方法displayReport()用于显示文字报表。

图1 初始设计方案结构图
在实际使用过程中发现该接口很不灵活,例如如果一个具体的数据显示类无须进行数据转换(源文件本身就是XML格式),但由于实现了该接口,将不得不实现其中声明的transformToXML()方法(至少需要提供一个空实现); 如果需要创建和显示图表,除了需实现与图表相关的方法外,还需要实现创建和显示文字报表的方法,否则程序编译时将报错。
现使用接口隔离原则对其进行重构。
在图1中,由于在接口CustomerDataDisplay中定义了太多方法,即该接口承担了太多职责,一方面导致该接口的实现类很庞大,在不同的实现类中都不得不实现接口中定义的所有方法,灵活性较差, 如果出现大量的空方法,将导致系统中产生大量的无用代码,影响代码质量; 另一方面由于客户端针对大接口编程,将在一定程序上破坏程序的封装性,客户端看到了不应该看到的方法,没有为客户端定制接口。 因此需要将该接口按照接口隔离原则和单一职责原则进行重构,将其中的一些方法封装在不同的小接口中,确保每一个接口使用起来都较为方便,并都承担某一单一角色,每个接口中只包含一个客户端(如模块或类)所需的方法即可。
通过使用接口隔离原则,本实例重构后的结构如图2所示:

图2 重构后的结构图
在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。 一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些它们不用的方法。
扩展 在《敏捷软件开发——原则、模式与实践》一书中,RobertC. Martin从解决“接口污染”的角度对接口隔离原则进行了详细的介绍,大家可以参阅该书第12章——_接口隔离原则(ISP)_进行深入的学习。
合成复用原则又称为组合/聚合复用原则(Composition/Aggregate Reuse Principle, CARP),其定义如下:
合成复用原则(Composite Reuse Principle, CRP):尽量使用对象组合,而不是继承来达到复用的目的。
合成复用原则就是在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分; 新对象通过委派调用已有对象的方法达到复用功能的目的。
简言之:复用时要尽量使用组合/聚合关系(关联关系),少用继承。
在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承, 但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少; 其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的内部细节通常对子类来说是可见的,所以这种复用又称“白箱”复用, 如果基类发生改变,那么子类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;而且继承只能在有限的环境中使用(如类没有声明为不能被继承)。
扩展 对于继承的深入理解,大家可以参考《软件架构设计》一书作者温昱先生的文章——《见山只是山见水只是水——提升对继承的认识》。
由于组合或聚合关系可以将已有的对象(也可称为成员对象)纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使得成员对象的内部实现细节对于新对象不可见,所以这种复用又称为“黑箱”复用,相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作; 合成复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。
一般而言,如果两个类之间是“Has-A”的关系应使用组合或聚合,如果是“Is-A”关系可使用继承。“Is-A"是严格的分类学意义上的定义,意思是一个类是另一个类的"一种”;而"Has-A"则不同,它表示某一个角色具有某一项责任。
下面通过一个简单实例来加深对合成复用原则的理解:
Sunny软件公司开发人员在初期的CRM系统设计中,考虑到客户数量不多,系统采用MySQL作为数据库,与数据库操作有关的类如CustomerDAO类等都需要连接数据库,连接数据库的方法getConnection()封装在DBUtil类中,由于需要重用DBUtil类的getConnection()方法,设计人员将CustomerDAO作为DBUtil类的子类,初始设计方案结构如图1所示:

图1 初始设计方案结构图
随着客户数量的增加,系统决定升级为Oracle数据库,因此需要增加一个新的OracleDBUtil类来连接Oracle数据库, 由于在初始设计方案中CustomerDAO和DBUtil之间是继承关系,因此在更换数据库连接方式时需要修改CustomerDAO类的源代码,将CustomerDAO作为OracleDBUtil的子类,这将违反开闭原则。 【当然也可以修改DBUtil类的源代码,同样会违反开闭原则。】
现使用合成复用原则对其进行重构。
根据合成复用原则,我们在实现复用时应该多用关联,少用继承。因此在本实例中我们可以使用关联复用来取代继承复用,重构后的结构如图2所示:

图2 重构后的结构图
在图2中,CustomerDAO和DBUtil之间的关系由继承关系变为关联关系,采用依赖注入的方式将DBUtil对象注入到CustomerDAO中,可以使用构造注入,也可以使用Setter注入。如果需要对DBUtil的功能进行扩展,可以通过其子类来实现,如通过子类OracleDBUtil来连接Oracle数据库。由于CustomerDAO针对DBUtil编程,根据里氏代换原则,DBUtil子类的对象可以覆盖DBUtil对象,只需在CustomerDAO中注入子类对象即可使用子类所扩展的方法。例如在CustomerDAO中注入OracleDBUtil对象,即可实现Oracle数据库连接,原有代码无须进行修改,而且还可以很灵活地增加新的数据库连接方式。
迪米特法则来自于1987年美国东北大学(Northeastern University)一个名为“Demeter”的研究项目。迪米特法则又称为最少知识原则(LeastKnowledge Principle, LKP),其定义如下:
迪米特法则(Law of Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用。
如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。
迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。
迪米特法则还有几种定义形式,包括**:**不要和“陌生人”说话、只与你的直接朋友通信等,在迪米特法则中,对于一个对象,其朋友包括以下几类:
当前对象本身(this);
以参数形式传入到当前对象方法中的对象;
当前对象的成员对象;
如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
当前对象所创建的对象。
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。
在应用迪米特法则时,一个对象只能与直接朋友发生交互,不要与“陌生人”发生直接交互,这样做可以降低系统的耦合度,一个对象的改变不会给太多其他对象带来影响。
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。
简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
在将迪米特法则运用到系统设计中时,要注意下面的几点:
在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;
在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;
在类的设计上,只要有可能,一个类型应当设计成不变类;
在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
下面通过一个简单实例来加深对迪米特法则的理解:
Sunny软件公司所开发CRM系统包含很多业务操作窗口,在这些窗口中,某些界面控件之间存在复杂的交互关系,一个控件事件的触发将导致多个其他界面控件产生响应,
例如,当一个按钮(Button)被单击时,对应的列表框(List)、组合框(ComboBox)、文本框(TextBox)、文本标签(Label)等都将发生改变,在初始设计方案中,界面控件之间的交互关系可简化为如图1所示结构:

图1 初始设计方案结构图
在图1中,由于界面控件之间的交互关系复杂,导致在该窗口中增加新的界面控件时需要修改与之交互的其他控件的源代码,系统扩展性较差,也不便于增加和删除新控件。
现使用迪米特对其进行重构。
在本实例中,可以通过引入一个专门用于控制界面控件交互的中间类(Mediator)来降低界面控件之间的耦合度。
引入中间类之后,界面控件之间不再发生直接引用,而是将请求先转发给中间类,再由中间类来完成对其他控件的调用。
当需要增加或删除新的控件时,只需修改中间类即可,无须修改新增控件或已有控件的源代码,重构后结构如图2所示:

图2 重构后的结构图
工厂模式是最常用的一类创建型设计模式,通常我们所说的工厂模式是指工厂方法模式,它也是使用频率最高的工厂模式。
本章将要学习的简单工厂模式是工厂方法模式的“小弟”,它不属于GoF 23种设计模式,但在软件开发中应用也较为频繁,通常将它作为学习其他工厂模式的入门。
此外,工厂方法模式还有一位“大哥”——抽象工厂模式。
这三种工厂模式各具特色,难度也逐个加大,在软件开发中它们都得到了广泛的应用,成为面向对象软件中常用的创建对象的工具。
如何构建简单工厂
首先将需要创建的各种不同对象的相关代码封装到不同的类中,这些类称为具体产品类,而将它们公共的代码进行抽象和提取后封装在一个抽象产品类中,每一个具体产品类都是抽象产品类的子类;
然后提供一个工厂类用于创建各种产品,在工厂类中提供一个创建产品的工厂方法,该方法可以根据所传入的参数不同创建不同的具体产品对象;
客户端只需调用工厂类的工厂方法并传入相应的参数即可得到一个产品对象。
简单工厂模式(Simple Factory Pattern):定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。因为在简单工厂模式中用于创建实例的方法是静态(static)方法,因此简单工厂模式又被称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。
简单工厂模式的要点在于:**当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。**简单工厂模式结构比较简单,其核心是工厂类的设计

**● Factory(工厂角色):**工厂角色即工厂类,它是简单工厂模式的核心,负责实现创建所有产品实例的内部逻辑;工厂类可以被外界直接调用,创建所需的产品对象;在工厂类中提供了静态的工厂方法factoryMethod(),它的返回类型为抽象产品类型Product。
**● Product(抽象产品角色):**它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象。
**● ConcreteProduct(具体产品角色):**它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。每一个具体产品角色都继承了抽象产品角色,需要实现在抽象产品中声明的抽象方法。
在简单工厂模式中,客户端通过工厂类来创建一个产品类的实例,而无须直接使用new关键字来创建对象,它是工厂模式家族中最简单的一员。
在使用简单工厂模式时,首先需要对产品类进行重构,不能设计一个包罗万象的产品类,而需根据实际情况设计一个产品层次结构,将所有产品类公共的代码移至抽象产品类,并在抽象产品类中声明一些抽象方法,以供不同的具体产品类来实现
public abstract class Product {
// 所有产品类的公共业务方法
public void methodSame() {
// 公共方法实现
}
// 声明抽象业务方法
public abstract void methodDiff();
}
在具体产品类中实现了抽象产品类中声明的抽象业务方法,不同的具体产品类可以提供不同的实现,典型的具体产品类代码如下所示:
public class ConcreteProductA extends Product{
@Override
public void methodDiff() {
// 业务方法实现
}
}
public class ConcreteProductB extends Product{
@Override
public void methodDiff() {
// 业务方法实现
}
}
简单工厂模式的核心是工厂类,在没有工厂类之前,客户端一般会使用new关键字来直接创建产品对象,而在引入工厂类之后,客户端可以通过工厂类来创建产品,在简单工厂模式中,工厂类提供了一个静态工厂方法供客户端使用,根据所传入的参数不同可以创建不同的产品对象,典型的工厂类代码如下所示:
public class Factory {
// 静态工厂方法
public static Product createProduct(String type) {
switch (type) {
case "A":
return new ConcreteProductA();
case "B":
return new ConcreteProductB();
default:
throw new RuntimeException("Invalid type");
}
}
}
在客户端代码中,我们通过调用工厂类的工厂方法即可得到产品对象,典型代码如下所示:
public class Client {
public static void main(String[] args) {
// 创建产品
Product product = Factory.createProduct("A");
product.methodSame();
product.methodDiff();
}
}
Sunny软件公司欲基于Java语言开发一套图表库,该图表库可以为应用系统提供各种不同外观的图表,例如柱状图、饼状图、折线图等。
Sunny软件公司图表库设计人员希望为应用系统开发人员提供一套灵活易用的图表库,而且可以较为方便地对图表库进行扩展,以便能够在将来增加一些新类型的图表。
Sunny软件公司图表库设计人员提出了一个初始设计方案,将所有图表的实现代码封装在一个Chart类中,其框架代码如下所示:
class Chart {
private String type; //图表类型
public Chart(Object[][] data, String type) {
this.type = type;
if (type.equalsIgnoreCase("histogram")) {
//初始化柱状图
}
else if (type.equalsIgnoreCase("pie")) {
//初始化饼状图
}
else if (type.equalsIgnoreCase("line")) {
//初始化折线图
}
}
public void display() {
if (this.type.equalsIgnoreCase("histogram")) {
//显示柱状图
}
else if (this.type.equalsIgnoreCase("pie")) {
//显示饼状图
}
else if (this.type.equalsIgnoreCase("line")) {
//显示折线图
}
}
}
客户端代码通过调用Chart类的构造函数来创建图表对象,根据参数type的不同可以得到不同类型的图表,然后再调用display()方法来显示相应的图表。
不难看出,Chart类是一个“巨大的”类,在该类的设计中存在如下几个问题:
在Chart类中包含很多“if…else…”代码块,整个类的代码相当冗长,代码越长,阅读难度、维护难度和测试难度也越大;而且大量条件语句的存在还将影响系统的性能,程序在执行过程中需要做大量的条件判断。
Chart类的职责过重,它负责初始化和显示所有的图表对象,将各种图表对象的初始化代码和显示代码集中在一个类中实现,违反了“单一职责原则”,不利于类的重用和维护;
而且将大量的对象初始化代码都写在构造函数中将导致构造函数非常庞大,对象在创建时需要进行条件判断,降低了对象创建的效率。
当需要增加新类型的图表时,必须修改Chart类的源代码,违反了“开闭原则”。
客户端只能通过new关键字来直接创建Chart对象,Chart类与客户端类耦合度较高,对象的创建和使用无法分离。
客户端在创建Chart对象之前可能还需要进行大量初始化设置,例如设置柱状图的颜色、高度等,如果在Chart类的构造函数中没有提供一个默认设置,那就只能由客户端来完成初始设置,这些代码在每次创建Chart对象时都会出现,导致代码的重复。
为了将Chart类的职责分离,同时将Chart对象的创建和使用分离,Sunny软件公司开发人员决定使用简单工厂模式对图表库进行重构,重构后的结构如图2所示:

在图2中,Chart接口充当抽象产品类,其子类HistogramChart、PieChart和LineChart充当具体产品类,ChartFactory充当工厂类。完整代码如下所示:
interface Chart {
void display();
}
/**
* 柱状图类:具体产品类
*/
public class HistogramChart implements Chart{
public HistogramChart() {
System.out.println("创建柱状图!");
}
@Override
public void display() {
System.out.println("显示柱状图!");
}
}
/**
* 折线图类:具体产品类
*/
class LineChart implements Chart {
public LineChart() {
System.out.println("创建折线图!");
}
@Override
public void display() {
System.out.println("显示折线图!");
}
}
/**
* 饼状图类:具体产品类
*/
public class PieChart implements Chart{
public PieChart() {
System.out.println("创建饼状图!");
}
@Override
public void display() {
System.out.println("显示饼状图!");
}
}
/**
* 图表工厂类:工厂类
*/
class ChartFactory {
//静态工厂方法
public static Chart getChart(String type) {
Chart chart = null;
if (type.equalsIgnoreCase("histogram")) {
chart = new HistogramChart();
System.out.println("初始化设置柱状图!");
}
else if (type.equalsIgnoreCase("pie")) {
chart = new PieChart();
System.out.println("初始化设置饼状图!");
}
else if (type.equalsIgnoreCase("line")) {
chart = new LineChart();
System.out.println("初始化设置折线图!");
}
return chart;
}
}
客户端调用
public class Client {
public static void main(String[] args) {
Chart chart;
chart = ChartFactory.getChart("histogram"); //通过静态工厂方法创建产品
chart.display();
}
}
输出
创建柱状图!
初始化设置柱状图!
显示柱状图!

在图3中,客户端可以通过产品父类的静态工厂方法,根据参数的不同创建不同类型的产品子类对象,这种做法在JDK等类库和框架中也广泛存在。
简单工厂模式提供了专门的工厂类用于创建对象,将对象的创建和对象的使用分离开,它作为一种最简单的工厂模式在软件开发中得到了较为广泛的应用。
简单工厂模式的主要优点如下:
工厂类包含必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的职责,而仅仅“消费”产品,简单工厂模式实现了对象创建和使用的分离。
客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以在一定程度减少使用者的记忆量。
简单工厂模式的主要缺点如下:
由于工厂类集中了所有产品的创建逻辑,职责过重,一旦不能正常工作,整个系统都要受到影响。
使用简单工厂模式势必会增加系统中类的个数(引入了新的工厂类),增加了系统的复杂度和理解难度。
系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。
简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
在以下情况下可以考虑使用简单工厂模式:
工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
客户端只知道传入工厂类的参数,对于如何创建对象并不关心。
练习
使用简单工厂模式设计一个可以创建不同几何形状(如圆形、方形和三角形等)的绘图工具,每个几何图形都具有绘制draw()和擦除erase()两个方法,要求在绘制不支持的几何图形时,提示一个UnSupportedShapeException。
简单工厂模式虽然简单,但存在一个很严重的问题。
当系统中需要引入新产品时,由于静态工厂方法通过所传入参数的不同来创建不同的产品,这必定要修改工厂类的源代码,将违背“开闭原则”,如何实现增加新产品而不影响已有代码?
工厂方法模式应运而生,本文将介绍第二种工厂模式——工厂方法模式。
在简单工厂模式中只提供一个工厂类,该工厂类处于对产品类进行实例化的中心位置,它需要知道每一个产品对象的创建细节,并决定何时实例化哪一个产品类。
简单工厂模式最大的缺点是当有新产品要加入到系统中时,必须修改工厂类,需要在其中加入必要的业务逻辑,这违背了“开闭原则”。
此外,在简单工厂模式中,所有的产品都由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性,而工厂方法模式则可以很好地解决这一问题。
在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。
工厂方法模式(Factory Method Pattern):定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式(Factory Pattern),又可称作虚拟构造器模式(Virtual Constructor Pattern)或多态工厂模式(Polymorphic Factory Pattern)。工厂方法模式是一种类创建型模式。
工厂方法模式提供一个抽象工厂接口来声明抽象工厂方法,而由其子类来具体实现工厂方法,创建具体的产品对象。

**● Product(抽象产品):**它是定义产品的接口,是工厂方法模式所创建对象的超类型,也就是产品对象的公共父类。
**● ConcreteProduct(具体产品):**它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。
**● Factory(抽象工厂):**在抽象工厂类中,声明了工厂方法(Factory Method),用于返回一个产品。抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口。
**● ConcreteFactory(具体工厂):**它是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。
与简单工厂模式相比,工厂方法模式最重要的区别是引入了抽象工厂角色,抽象工厂可以是接口,也可以是抽象类或者具体类
public interface Factory {
Product factoryMethod();
}
在抽象工厂中声明了工厂方法但并未实现工厂方法,具体产品对象的创建由其子类负责,客户端针对抽象工厂编程,可在运行时再指定具体工厂类,具体工厂类实现了工厂方法,不同的具体工厂可以创建不同的具体产品,其典型代码如下所示:
public class ConcreteFactory implements Factory {
@Override
public Product factoryMethod() {
return new ConcreteProduct();
}
}
在实际使用时,具体工厂类在实现工厂方法时除了创建具体产品对象之外,还可以负责产品对象的初始化工作以及一些资源和环境配置工作,例如连接数据库、创建文件等。
在客户端代码中,只需关心工厂类即可,不同的具体工厂可以创建不同的产品,典型的客户端类代码片段如下所示:
public class Client {
public static void main(String[] args) {
Factory factory = new ConcreteFactory(); //可通过配置文件实现
Product product;
product = factory.factoryMethod();
}
}
Sunny软件公司欲开发一个系统运行日志记录器(Logger),该记录器可以通过多种途径保存系统的运行日志,如通过文件记录或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,Sunny公司的开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的设置过程较为复杂,而且某些参数的设置有严格的先后次序,否则可能会发生记录失败。如何封装记录器的初始化过程并保证多种记录器切换的灵活性是Sunny公司开发人员面临的一个难题。
Sunny公司的开发人员通过对该需求进行分析,发现该日志记录器有两个设计要点:
需要封装日志记录器的初始化过程,这些初始化工作较为复杂,例如需要初始化其他相关的类,还有可能需要读取配置文件(例如连接数据库或创建文件),导致代码较长,如果将它们都写在构造函数中,会导致构造函数庞大,不利于代码的修改和维护;
用户可能需要更换日志记录方式,在客户端代码中需要提供一种灵活的方式来选择日志记录器,尽量在不修改源代码的基础上更换或者增加日志记录方式。
Sunny公司开发人员最初使用简单工厂模式对日志记录器进行了设计,初始结构:

在图中,LoggerFactory充当创建日志记录器的工厂,提供了工厂方法createLogger()用于创建日志记录器,Logger是抽象日志记录器接口,其子类为具体日志记录器。其中,工厂类LoggerFactory代码片段如下所示:
//日志记录器工厂
class LoggerFactory {
//静态工厂方法
public static Logger createLogger(String args) {
if(args.equalsIgnoreCase("db")) {
//连接数据库,代码省略
//创建数据库日志记录器对象
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}
else if(args.equalsIgnoreCase("file")) {
//创建日志文件
//创建文件日志记录器对象
Logger logger = new FileLogger();
//初始化文件日志记录器,代码省略
return logger;
}
else {
return null;
}
}
}
为了突出设计重点,我们对上述代码进行了简化,省略了具体日志记录器类的初始化代码。在LoggerFactory类中提供了静态工厂方法createLogger(),用于根据所传入的参数创建各种不同类型的日志记录器。
通过使用简单工厂模式,我们将日志记录器对象的创建和使用分离,客户端只需使用由工厂类创建的日志记录器对象即可,无须关心对象的创建过程,但是我们发现,虽然简单工厂模式实现了对象的创建和使用分离,但是仍然存在如下两个问题:
工厂类过于庞大,包含了大量的if…else…代码,导致维护和测试难度增大;
系统扩展不灵活,如果增加新类型的日志记录器,必须修改静态工厂方法的业务逻辑,违反了“开闭原则”。
如何解决这两个问题,提供一种简单工厂模式的改进方案?这就是本文所介绍的工厂方法模式的动机之一。
Sunny公司开发人员决定使用工厂方法模式来设计日志记录器,其基本结构:

Logger接口充当抽象产品,其子类FileLogger和DatabaseLogger充当具体产品,LoggerFactory接口充当抽象工厂,其子类FileLoggerFactory和DatabaseLoggerFactory充当具体工厂。
完整代码如下所示:
public interface Logger {
void wirteLog();
}
public interface LoggerFactory {
Logger createLogger();
}
public class DatabaseLogger implements Logger{
@Override
public void wirteLog() {
System.out.println("数据库日志记录");
}
}
public class DatabaseLoggerFactory implements LoggerFactory{
@Override
public Logger createLogger() {
// 连接数据库,略
// 创建数据库日志记录器对象
Logger logger = new DatabaseLogger();
// 其他初始化逻辑略
return logger;
}
}
public class FileLogger implements Logger{
@Override
public void wirteLog() {
System.out.println("文件日志记录");
}
}
public class FileLoggerFactory implements LoggerFactory{
@Override
public Logger createLogger() {
// 创建文件日志记录器对象
Logger logger = new FileLogger();
// 创建文件等其他初始化代码,略
return logger;
}
}
编写如下客户端测试代码:
public class Client {
public static void main(String[] args) {
LoggerFactory factory;
Logger logger;
factory = new FileLoggerFactory();
logger = factory.createLogger();
logger.wirteLog();
}
}
编译并运行程序,输出结果如下:
文件日志记录。
interface LoggerFactory {
public Logger createLogger();
public Logger createLogger(String args);
public Logger createLogger(Object obj);
}
具体工厂类DatabaseLoggerFactory代码修改如下:
class DatabaseLoggerFactory implements LoggerFactory {
public Logger createLogger() {
//使用默认方式连接数据库,代码省略
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}
public Logger createLogger(String args) {
//使用参数args作为连接字符串来连接数据库,代码省略
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}
public Logger createLogger(Object obj) {
//使用封装在参数obj中的连接字符串来连接数据库,代码省略
Logger logger = new DatabaseLogger();
//使用封装在参数obj中的数据来初始化数据库日志记录器,代码省略
return logger;
}
}
//其他具体工厂类代码省略
在抽象工厂中定义多个重载的工厂方法,在具体工厂中实现了这些工厂方法,这些方法可以包含不同的业务逻辑,以满足对不同产品对象的需求。
但是这就要求了所有日志方式都要支持三种初始化方式,这可能不符合实际需求。因为有些可能没有空参初始化或者sring args初始化方式的日志记录方式。这样太死板了。
有时候,为了进一步简化客户端的使用,还可以对客户端隐藏工厂方法,此时,在工厂类中将直接调用产品类的业务方法,客户端无须调用工厂方法创建产品,直接通过工厂即可使用所创建的对象中的业务方法。
如果对客户端隐藏工厂方法,日志记录器的结构图将修改为图5所示:

图5 隐藏工厂方法后的日志记录器结构图
在图5中,抽象工厂类LoggerFactory的代码修改如下:
public abstract class LoggerFactory {
//在工厂类中直接调用日志记录器类的业务方法writeLog()
public void writeLog() {
Logger logger = this.createLogger();
logger.wirteLog();
}
public abstract Logger createLogger();
}
// 对应的DatabaseLoggerFactory 、FileLoggerFactory 由实现接口改成继承抽象
public class DatabaseLoggerFactory extends LoggerFactory {
@Override
public Logger createLogger() {
// 连接数据库,略
// 创建数据库日志记录器对象
Logger logger = new DatabaseLogger();
// 其他初始化逻辑略
return logger;
}
}
public class FileLoggerFactory extends LoggerFactory {
@Override
public Logger createLogger() {
// 创建文件日志记录器对象
Logger logger = new FileLogger();
// 创建文件等其他初始化代码,略
return logger;
}
}
客户端代码修改如下:
public class Client {
public static void main(String[] args) {
LoggerFactory factory = new DatabaseLoggerFactory();
factory.writeLog();
}
}
通过将业务方法的调用移入工厂类,可以直接使用工厂对象来调用产品对象的业务方法,客户端无须直接使用工厂方法,在某些情况下我们也可以使用这种设计方案。
工厂方法模式是简单工厂模式的延伸,它继承了简单工厂模式的优点,同时还弥补了简单工厂模式的不足。工厂方法模式是使用频率最高的设计模式之一,是很多开源框架和API类库的核心模式。
工厂方法模式的主要优点如下:
在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够让工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,就正是因为所有的具体工厂类都具有同一抽象父类。
使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了,这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
工厂方法模式的主要缺点如下:
在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,增加了系统的实现难度。
在以下情况下可以考虑使用工厂方法模式:
客户端不知道它所需要的对象的类。在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建,可将具体工厂类的类名存储在配置文件或数据库中。
抽象工厂类通过其子类来指定创建哪个对象。在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
练习
使用工厂方法模式设计一个程序来读取各种不同类型的图片格式,针对每一种图片格式都设计一个图片读取器,如GIF图片读取器用于读取GIF格式的图片、JPG图片读取器用于读取JPG格式的图片。
需充分考虑系统的灵活性和可扩展性。
抽象工厂模式为创建一组对象提供了一种解决方案。与工厂方法模式相比,抽象工厂模式中的具体工厂不只是创建一种产品,它负责创建一族产品。
抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,它是一种对象创建型模式。
在抽象工厂模式中,每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的产品,这些产品构成了一个产品族

**● AbstractFactory(抽象工厂):**它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。
**● ConcreteFactory(具体工厂):**它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。
**● AbstractProduct(抽象产品):**它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。
**● ConcreteProduct(具体产品):**它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。
在抽象工厂中声明了多个工厂方法,用于创建不同类型的产品,抽象工厂可以是接口,也可以是抽象类或者具体类
abstract class AbstractFactory {
public abstract AbstractProductA createProductA(); //工厂方法一
public abstract AbstractProductB createProductB(); //工厂方法二
……
}
具体工厂实现了抽象工厂,每一个具体的工厂方法可以返回一个特定的产品对象,而同一个具体工厂所创建的产品对象构成了一个产品族。
对于每一个具体工厂类,其典型代码如下所示:
class ConcreteFactory1 extends AbstractFactory {
//工厂方法一
public AbstractProductA createProductA() {
return new ConcreteProductA1();
}
//工厂方法二
public AbstractProductB createProductB() {
return new ConcreteProductB1();
}
……
}
与工厂方法模式一样,抽象工厂模式也可为每一种产品提供一组重载的工厂方法,以不同的方式对产品对象进行创建。
在工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法具有唯一性,一般情况下,一个具体工厂中只有一个或者一组重载的工厂方法。
但是有时候我们希望一个工厂可以提供多个产品对象,而不是单一的产品对象,
如一个电器工厂,它可以生产电视机、电冰箱、空调等多种电器,而不是只生产某一种电器。
为了更好地理解抽象工厂模式,我们先引入两个概念:
产品等级结构:产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。
产品族:在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中,海尔电视机、海尔电冰箱构成了一个产品族。
产品等级结构与产品族示意图:

不同颜色的多个正方形、圆形和椭圆形分别构成了三个不同的产品等级结构,而相同颜色的正方形、圆形和椭圆形构成了一个产品族,每一个形状对象都位于某个产品族,并属于某个产品等级结构。图3中一共有五个产品族,分属于三个不同的产品等级结构。
我们只要指明一个产品所处的产品族以及它所属的等级结构,就可以唯一确定这个产品。
当系统所提供的工厂生产的具体产品并不是一个简单的对象,而是多个位于不同产品等级结构、属于不同类型的具体产品时就可以使用抽象工厂模式。
**抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形式。
**抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建。
**当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、更有效率。
**抽象工厂模式示意图:

每一个具体工厂可以生产属于一个产品族的所有产品,例如生产颜色相同的正方形、圆形和椭圆形,所生产的产品又位于不同的产品等级结构中。如果使用工厂方法模式,图4所示结构需要提供15个具体工厂,而使用抽象工厂模式只需要提供5个具体工厂,极大减少了系统中类的个数。
Sunny软件公司欲开发一套界面皮肤库,可以对Java桌面软件进行界面美化。
为了保护版权,该皮肤库源代码不打算公开,而只向用户提供已打包为jar文件的class字节码文件。
用户在使用时可以通过菜单来选择皮肤,不同的皮肤将提供视觉效果不同的按钮、文本框、组合框等界面元素,其结构示意图:

该皮肤库需要具备良好的灵活性和可扩展性,用户可以自由选择不同的皮肤,开发人员可以在不修改既有代码的基础上增加新的皮肤。
Sunny软件公司的开发人员针对上述要求,决定使用工厂方法模式进行系统的设计,为了保证系统的灵活性和可扩展性,提供一系列具体工厂来创建按钮、文本框、组合框等界面元素,客户端针对抽象工厂编程,初始结构:

提供了大量工厂来创建具体的界面组件,可以通过配置文件更换具体界面组件从而改变界面风格。
但是,此设计方案存在如下问题:
当需要增加新的皮肤时,虽然不要修改现有代码,但是需要增加大量类,针对每一个新增具体组件都需要增加一个具体工厂,类的个数成对增加,这无疑会导致系统越来越庞大,增加系统的维护成本和运行开销;
由于同一种风格的具体界面组件通常要一起显示,因此需要为每个组件都选择一个具体工厂,用户在使用时必须逐个进行设置,如果某个具体工厂选择失误将会导致界面显示混乱,虽然我们可以适当增加一些约束语句,但客户端代码和配置文件都较为复杂。
如何减少系统中类的个数并保证客户端每次始终只使用某一种风格的具体界面组件?这是Sunny公司开发人员所面临的两个问题,显然,工厂方法模式无法解决这两个问题
Sunny公司开发人员使用抽象工厂模式来重构界面皮肤库的设计,其基本结构图:

SkinFactory接口充当抽象工厂,
其子类SpringSkinFactory和SummerSkinFactory充当具体工厂,
接口Button、TextField和ComboBox充当抽象产品,
其子类SpringButton、SpringTextField、SpringComboBox和SummerButton、SummerTextField、SummerComboBox充当具体产品。
完整代码如下所示:
//在本实例中我们对代码进行了大量简化,实际使用时,界面组件的初始化代码较为复杂,还需要使用JDK中一些已有类,为了突出核心代码,在此只提供框架代码和演示输出。
//按钮接口:抽象产品
interface Button {
public void display();
}
//Spring按钮类:具体产品
class SpringButton implements Button {
public void display() {
System.out.println("显示浅绿色按钮。");
}
}
//Summer按钮类:具体产品
class SummerButton implements Button {
public void display() {
System.out.println("显示浅蓝色按钮。");
}
}
//文本框接口:抽象产品
interface TextField {
public void display();
}
//Spring文本框类:具体产品
class SpringTextField implements TextField {
public void display() {
System.out.println("显示绿色边框文本框。");
}
}
//Summer文本框类:具体产品
class SummerTextField implements TextField {
public void display() {
System.out.println("显示蓝色边框文本框。");
}
}
//组合框接口:抽象产品
interface ComboBox {
public void display();
}
//Spring组合框类:具体产品
class SpringComboBox implements ComboBox {
public void display() {
System.out.println("显示绿色边框组合框。");
}
}
//Summer组合框类:具体产品
class SummerComboBox implements ComboBox {
public void display() {
System.out.println("显示蓝色边框组合框。");
}
}
//界面皮肤工厂接口:抽象工厂
interface SkinFactory {
public Button createButton();
public TextField createTextField();
public ComboBox createComboBox();
}
//Spring皮肤工厂:具体工厂
class SpringSkinFactory implements SkinFactory {
public Button createButton() {
return new SpringButton();
}
public TextField createTextField() {
return new SpringTextField();
}
public ComboBox createComboBox() {
return new SpringComboBox();
}
}
//Summer皮肤工厂:具体工厂
class SummerSkinFactory implements SkinFactory {
public Button createButton() {
return new SummerButton();
}
public TextField createTextField() {
return new SummerTextField();
}
public ComboBox createComboBox() {
return new SummerComboBox();
}
}
编写如下客户端测试代码:
public class Client {
public static void main(String[] args) {
//使用抽象层定义
SkinFactory factory;
Button bt;
TextField tf;
ComboBox cb;
//通过类名生成实例对象并将其返回
//SpringSkinFactory, SummerSkinFactory
factory = new SummerSkinFactory();
bt = factory.createButton();
tf = factory.createTextField();
cb = factory.createComboBox();
bt.display();
tf.display();
cb.display();
}
}
编译并运行程序,输出结果如下:
显示浅蓝色按钮。
显示蓝色边框文本框。
显示蓝色边框组合框。
如果需要更换皮肤,只需修改配置文件即可,在实际环境中,我们可以提供可视化界面,例如菜单或者窗口来修改配置文件,用户无须直接修改配置文件。
如果需要增加新的皮肤,只需增加一族新的具体组件并对应提供一个新的具体工厂,修改配置文件即可使用新的皮肤,原有代码无须修改,符合“开闭原则”。
Sunny公司使用抽象工厂模式设计了界面皮肤库,该皮肤库可以较为方便地增加新的皮肤,但是现在遇到一个非常严重的问题:由于设计时考虑不全面,忘记为单选按钮(RadioButton)提供不同皮肤的风格化显示,导致无论选择哪种皮肤,单选按钮都显得那么“格格不入”。
Sunny公司的设计人员决定向系统中增加单选按钮,但是发现原有系统居然不能够在符合“开闭原则”的前提下增加新的组件,原因是抽象工厂SkinFactory中根本没有提供创建单选按钮的方法,如果需要增加单选按钮,首先需要修改抽象工厂接口SkinFactory,在其中新增声明创建单选按钮的方法,然后逐个修改具体工厂类,增加相应方法以实现在不同的皮肤中创建单选按钮,此外还需要修改客户端,否则单选按钮无法应用于现有系统。
怎么办?答案是抽象工厂模式无法解决该问题,这也是抽象工厂模式最大的缺点。
在抽象工厂模式中,增加新的产品族很方便,但是增加新的产品等级结构很麻烦,抽象工厂模式的这种性质称为**“开闭原则”的倾斜性**。
“开闭原则”要求系统对扩展开放,对修改封闭,通过扩展达到增强其功能的目的,对于涉及到多个产品族与多个产品等级结构的系统,其功能增强包括两方面:
增加产品族:对于增加新的产品族,抽象工厂模式很好地支持了“开闭原则”,只需要增加具体产品并对应增加一个新的具体工厂,对已有代码无须做任何修改。
增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了“开闭原则”。
正因为抽象工厂模式存在“开闭原则”的倾斜性,它以一种倾斜的方式来满足“开闭原则”,为增加新产品族提供方便,但不能为增加新产品结构提供这样的方便,因此要求设计人员在设计之初就能够全面考虑,不会在设计完成之后向系统中增加新的产品等级结构,也不会删除已有的产品等级结构,否则将会导致系统出现较大的修改,为后续维护工作带来诸多麻烦。
抽象工厂模式是工厂方法模式的进一步延伸,由于它提供了功能更为强大的工厂类并且具备较好的可扩展性,在软件开发中得以广泛应用,尤其是在一些框架和API类库的设计中,例如在Java语言的AWT(抽象窗口工具包)中就使用了抽象工厂模式,它使用抽象工厂模式来实现在不同的操作系统中应用程序呈现与所在操作系统一致的外观界面。
抽象工厂模式也是在软件开发中最常用的设计模式之一。
抽象工厂模式的主要优点如下:
抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。
增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。
抽象工厂模式的主要缺点如下:
增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。
在以下情况下可以考虑使用抽象工厂模式:
一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是很重要的,用户无须关心对象的创建过程,将对象的创建和使用解耦。
系统中有多于一个的产品族,而每次只使用其中某一产品族。可以通过配置文件等方式来使得用户可以动态改变产品族,也可以很方便地增加新的产品族。
属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。同一个产品族中的产品可以是没有任何关系的对象,但是它们都具有一些共同的约束,如同一操作系统下的按钮和文本框,按钮与文本框之间没有直接关系,但它们都是属于某一操作系统的,此时具有一个共同的约束条件:操作系统的类型。
产品等级结构稳定,设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构。
Sunny软件公司欲推出一款新的手机游戏软件,该软件能够支持Symbian、Android和Windows Mobile等多个智能手机操作系统平台,针对不同的手机操作系统,该游戏软件提供了不同的游戏操作控制(OperationController)类和游戏界面控制(InterfaceController)类,并提供相应的工厂类来封装这些类的初始化过程。软件要求具有较好的扩展性以支持新的操作系统平台,为了满足上述需求,试采用抽象工厂模式对其进行设计。
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
单例模式有三个要点:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
单例模式是结构最简单的设计模式一,在它的核心结构中只包含一个被称为单例类的特殊类。

● Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。

Windows任务管理器
我们可以做一个这样的尝试,在Windows的“任务栏”的右键弹出菜单上多次点击“启动任务管理器”,看能否打开多个任务管理器窗口?
如果你的桌面出现多个任务管理器,我请你吃饭(注:电脑中毒或私自修改Windows内核者除外)。
通常情况下,无论我们启动任务管理多少次,Windows系统始终只能弹出一个任务管理器窗口,也就是说在一个Windows系统中,任务管理器存在唯一性。
为什么要这样设计呢?我们可以从以下两个方面来分析:
其一,如果能弹出多个窗口,且这些窗口的内容完全一致,全部是重复对象,这势必会浪费系统资源,任务管理器需要获取系统运行时的诸多信息,这些信息的获取需要消耗一定的系统资源,包括CPU资源及内存资源等,浪费是可耻的,而且根本没有必要显示多个内容完全相同的窗口;
其二,如果弹出的多个窗口内容不一致,问题就更加严重了,这意味着在某一瞬间系统资源使用情况和进程、服务等信息存在多个状态,例如任务管理器窗口A显示“CPU使用率”为10%,窗口B显示“CPU使用率”为15%,到底哪个才是真实的呢?这纯属“调戏”用户,给用户带来误解,更不可取。
由此可见,确保Windows任务管理器在系统中有且仅有一个非常重要。
回到实际开发中,我们也经常遇到类似的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,我们可以通过单例模式来实现,这就是单例模式的动机所在。
下面我们来模拟实现Windows任务管理器,假设任务管理器的类名为TaskManager,在TaskManager类中包含了大量的成员方法,例如构造函数TaskManager(),显示进程的方法displayProcesses(),显示服务的方法displayServices()等,该类的示意代码如下:
class TaskManager
{
public TaskManager() {……} //初始化窗口
public void displayProcesses() {……} //显示进程
public void displayServices() {……} //显示服务
……
}
为了实现Windows任务管理器的唯一性,我们通过如下三步来对该类进行重构:
private TaskManager() {……}
private static TaskManager tm = null;
public static TaskManager getInstance()
{
if (tm == null)
{
tm = new TaskManager();
}
return tm;
}
在getInstance()方法中首先判断tm对象是否存在,如果不存在(即tm == null),则使用new关键字创建一个新的TaskManager类型的tm对象,再返回新创建的tm对象;否则直接返回已有的tm对象。
需要注意的是getInstance()方法的修饰符,首先它应该是一个public方法,以便供外界其他对象使用,其次它使用了static关键字,即它是一个静态方法,在类外可以直接通过类名来访问,而无须创建TaskManager对象,事实上在类外也无法创建TaskManager对象,因为构造函数是私有的。
通过以上三个步骤,我们完成了一个最简单的单例类的设计,其完整代码如下:
class TaskManager
{
private static TaskManager tm = null;
private TaskManager() {……} //初始化窗口
public void displayProcesses() {……} //显示进程
public void displayServices() {……} //显示服务
public static TaskManager getInstance()
{
if (tm == null)
{
tm = new TaskManager();
}
return tm;
}
……
}
在类外我们无法直接创建新的TaskManager对象,但可以通过代码TaskManager.getInstance()来访问实例对象,第一次调用getInstance()方法时将创建唯一实例,再次调用时将返回第一次创建的实例,从而确保实例对象的唯一性。
Sunny软件公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。
由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。
如何确保负载均衡器的唯一性是该软件成功的关键。
Sunny公司开发人员通过分析和权衡,决定使用单例模式来设计该负载均衡器,结构图:

在图中,将负载均衡器LoadBalancer设计为单例类,其中包含一个存储服务器信息的集合serverList,每次在serverList中随机选择一台服务器来响应客户端的请求,实现代码如下所示:
import java.util.*;
//负载均衡器LoadBalancer:单例类,真实环境下该类将非常复杂,包括大量初始化的工作和业务方法,考虑到代码的可读性和易理解性,只列出部分与模式相关的核心代码
class LoadBalancer {
//私有静态成员变量,存储唯一实例
private static LoadBalancer instance = null;
//服务器集合
private List serverList = null;
//私有构造函数
private LoadBalancer() {
serverList = new ArrayList();
}
//公有静态成员方法,返回唯一实例
public static LoadBalancer getLoadBalancer() {
if (instance == null) {
instance = new LoadBalancer();
}
return instance;
}
//增加服务器
public void addServer(String server) {
serverList.add(server);
}
//删除服务器
public void removeServer(String server) {
serverList.remove(server);
}
//使用Random类随机获取服务器
public String getServer() {
Random random = new Random();
int i = random.nextInt(serverList.size());
return (String)serverList.get(i);
}
}
客户端测试代码
class Client {
public static void main(String args[]) {
//创建四个LoadBalancer对象
LoadBalancer balancer1,balancer2,balancer3,balancer4;
balancer1 = LoadBalancer.getLoadBalancer();
balancer2 = LoadBalancer.getLoadBalancer();
balancer3 = LoadBalancer.getLoadBalancer();
balancer4 = LoadBalancer.getLoadBalancer();
//判断服务器负载均衡器是否相同
if (balancer1 == balancer2 && balancer2 == balancer3 && balancer3 == balancer4) {
System.out.println("服务器负载均衡器具有唯一性!");
}
//增加服务器
balancer1.addServer("Server 1");
balancer1.addServer("Server 2");
balancer1.addServer("Server 3");
balancer1.addServer("Server 4");
//模拟客户端请求的分发
for (int i = 0; i < 10; i++) {
String server = balancer1.getServer();
System.out.println("分发请求至服务器: " + server);
}
}
}
Sunny公司开发人员使用单例模式实现了负载均衡器的设计,但是在实际使用中出现了一个非常严重的问题,当负载均衡器在启动过程中用户再次启动该负载均衡器时,系统无任何异常,但当客户端提交请求时出现请求分发失败,通过仔细分析发现原来系统中还是存在多个负载均衡器对象,导致分发时目标服务器不一致,从而产生冲突。
为什么会这样呢?Sunny公司开发人员百思不得其解。
现在我们对负载均衡器的实现代码进行再次分析,当第一次调用getLoadBalancer()方法创建并启动负载均衡器时,instance对象为null值,因此系统将执行代码instance= new LoadBalancer(),在此过程中,由于要对LoadBalancer进行大量初始化工作,需要一段时间来创建LoadBalancer对象。
而在此时,如果再一次调用getLoadBalancer()方法(通常发生在多线程环境中),由于instance尚未创建成功,仍为null值,判断条件(instance== null)为真值,因此代码instance= new LoadBalancer()将再次执行,导致最终创建了多个instance对象,这违背了单例模式的初衷,也导致系统运行发生错误。
要解决这个问题,现引入两种单例解决方案: 饿汉式单例和懒汉式单例
饿汉式单例类是实现起来最简单的单例类,饿汉式单例类结构图:

由于在定义静态变量的时候实例化单例类,因此在类加载的时候就已经创建了单例对象,代码如下所示:
class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() { }
public static EagerSingleton getInstance() {
return instance;
}
}
当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。如果使用饿汉式单例来实现负载均衡器LoadBalancer类的设计,则不会出现创建多个单例对象的情况,可确保单例对象的唯一性。
除了饿汉式单例,还有一种经典的懒汉式单例,也就是前面的负载均衡器LoadBalancer类的实现方式。懒汉式单例类结构图:

懒汉式单例在第一次调用getInstance()方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载(Lazy Load)技术,即需要的时候再加载实例,为了避免多个线程同时调用getInstance()方法,我们可以使用关键字synchronized,代码如下所示:
class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() { }
synchronized public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
该懒汉式单例类在getInstance()方法前面增加了关键字synchronized进行线程锁,以处理多个线程同时访问的问题。但是,上述代码虽然解决了线程安全问题,但是每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。如何既解决线程安全问题又不影响系统性能呢?我们继续对懒汉式单例进行改进。事实上,我们无须对整个getInstance()方法进行锁定,只需对其中的代码“instance = new LazySingleton();”进行锁定即可。因此getInstance()方法可以进行如下改进:
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}
问题貌似得以解决,事实并非如此。如果使用以上代码来实现单例,还是会存在单例对象不唯一。
原因如下:
假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过instance == null的判断。
由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。
但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在synchronized中再进行一次(instance == null)判断,这种方式称为双重检查锁定(Double-Check Locking)。
使用双重检查锁定实现的懒汉式单例类完整代码如下所示:
class LazySingleton {
private volatile static LazySingleton instance = null;
private LazySingleton() { }
public static LazySingleton getInstance() {
//第一重判断
if (instance == null) {
//锁定代码块
synchronized (LazySingleton.class) {
//第二重判断
if (instance == null) {
instance = new LazySingleton(); //创建单例实例
}
}
}
return instance;
}
}
需要注意的是,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理,且该代码只能在JDK 1.5及以上版本中才能正确执行。由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。
扩展
IBM公司高级软件工程师Peter Haggar 2004年在IBM developerWorks上发表了一篇名为《双重检查锁定及单例模式——全面理解这一失效的编程习语》的文章,对JDK 1.5之前的双重检查锁定及单例模式进行了全面分析和阐述,参考链接:http://www.ibm.com/developerworks/cn/java/j-dcl.html
饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。
但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。(但其实在现在的软件系统中,本来也是需要一些预热。饿汉式这种加载方式其实没什么缺点,更不需要懒汉式那么复杂的双重检查判断逻辑,理解起来也更容易)
懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。
但是在现在的软件系统中, 既然用到了单例那么在启动系统的时候初始化出来其实不是什么缺点。 云服务时代的应用本来启动也不是秒级。饿汉式单例占用的时间基本忽略不计。再说本来我们的系统也要进行一些数据预热。就算是懒汉式也会让他初始化一个实例出来。 而且饿汉式代码简单。
饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。可见,无论是饿汉式单例还是懒汉式单例都存在这样那样的问题,有没有一种方法,能够将两种单例的缺点都克服,而将两者的优点合二为一呢?答案是:Yes!下面我们来学习这种更好的被称之为Initialization Demand Holder (IoDH)的技术。
在IoDH中,我们在单例类中增加一个静态(static)内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用,实现代码如下所示:
//Initialization on Demand Holder
class Singleton {
private Singleton() {
}
private static class HolderClass {
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return HolderClass.instance;
}
public static void main(String args[]) {
Singleton s1, s2;
s1 = Singleton.getInstance();
s2 = Singleton.getInstance();
System.out.println(s1==s2);
}
}
编译并运行上述代码,运行结果为:true,即创建的单例对象s1和s2为同一对象。由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。
通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH)。
单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。
单例模式的主要优点如下:
单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
单例模式的主要缺点如下:
由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
在以下情况下可以考虑使用单例模式:
系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
如何对单例模式进行改造,使得系统中某个类的对象可以存在有限多个,例如两例或三例?【注:改造之后的类可称之为多例类。】
在使用原型模式时,我们需要首先创建一个原型对象,再通过复制这个原型对象来创建更多同类型的对象。
试想,如果连孙悟空的模样都不知道,怎么拔毛变小猴子呢?
原型模式(Prototype Pattern):使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式。
原型模式的工作原理很简单:将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程。
由于在软件系统中我们经常会遇到需要创建多个相同或者相似对象的情况,因此原型模式在真实开发中的使用频率还是非常高的。
原型模式是一种“另类”的创建型模式,创建克隆对象的工厂就是原型类自身,工厂方法由克隆方法来实现。
需要注意的是通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常对克隆所产生的对象进行修改对原型对象不会造成任何影响,每一个克隆对象都是相互独立的。
通过不同的方式修改可以得到一系列相似但不完全相同的对象。

●Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。
● ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。
● Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。
原型模式的核心在于如何实现克隆方法,下面将介绍两种在Java语言中常用的克隆实现方法:
通用的克隆实现方法是在具体原型类的克隆方法中实例化一个与自身类型相同的对象并将其返回,并将相关的参数传入新创建的对象中,保证它们的成员属性相同。示意代码如下所示:
class ConcretePrototype implements Prototype
{
private String attr; //成员属性
public void setAttr(String attr)
{
this.attr = attr;
}
public String getAttr()
{
return this.attr;
}
public Prototype clone() //克隆方法
{
Prototype prototype = new ConcretePrototype(); //创建新对象
prototype.setAttr(this.attr);
return prototype;
}
}
思考
能否将上述代码中的clone()方法写成:public Prototype clone() { return this; }?给出你的理由。
在客户类中我们只需要创建一个ConcretePrototype对象作为原型对象,然后调用其clone()方法即可得到对应的克隆对象,如下代码所示:
Prototype obj1 = new ConcretePrototype();
obj1.setAttr("Sunny");
Prototype obj2 = obj1.clone();
这种方法可作为原型模式的通用实现,它与编程语言特性无关,任何面向对象语言都可以使用这种形式来实现对原型的克隆。
学过Java语言的人都知道,所有的Java类都继承自java.lang.Object。事实上,Object类提供一个clone()方法,可以将一个Java对象复制一份。
因此在Java中可以直接使用Object提供的clone()方法来实现对象的克隆,Java语言中的原型模式实现很简单。
需要注意的是能够实现克隆的Java类必须实现一个标识接口Cloneable,表示这个Java类支持被复制。如果一个类没有实现这个接口但是调用了clone()方法,Java编译器将抛出一个CloneNotSupportedException异常。如下代码所示:
class ConcretePrototype implements Cloneable
{
……
public Prototype clone()
{
Object object = null;
try {
object = super.clone();
} catch (CloneNotSupportedException exception) {
System.err.println("Not support cloneable");
}
return (Prototype )object;
}
……
}
在客户端创建原型对象和克隆对象也很简单,如下代码所示:
Prototype obj1 = new ConcretePrototype();
Prototype obj2 = obj1.clone();
一般而言,Java语言中的clone()方法满足:
对任何对象x,都有x.clone() != x,即克隆对象与原型对象不是同一个对象;
对任何对象x,都有x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型一样;
如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。
为了获取对象的一份拷贝,我们可以直接利用Object类的clone()方法,具体步骤如下:
在派生类中覆盖基类的clone()方法,并声明为public;
在派生类的clone()方法中,调用super.clone();
派生类需实现Cloneable接口。
此时,Object类相当于抽象原型类,所有实现了Cloneable接口的类相当于具体原型类。
Sunny软件公司一直使用自行开发的一套OA (Office Automatic,办公自动化)系统进行日常工作办理,但在使用过程中,越来越多的人对工作周报的创建和编写模块产生了抱怨。
追其原因,Sunny软件公司的OA管理员发现,由于某些岗位每周工作存在重复性,工作周报内容都大同小异,工作周报示意图如下。

这些周报只有一些小地方存在差异,但是现行系统每周默认创建的周报都是空白报表,用户只能通过重新输入或不断复制粘贴来填写重复的周报内容,极大降低了工作效率,浪费宝贵的时间。如何快速创建相同或者相似的工作周报,成为Sunny公司OA开发人员面临的一个新问题。
Sunny公司的开发人员通过对问题进行仔细分析,决定按照如下思路对工作周报模块进行重新设计和实现:
除了允许用户创建新周报外,还允许用户将创建好的周报保存为模板;
用户在再次创建周报时,可以创建全新的周报,还可以选择合适的模板复制生成一份相同的周报,然后对新生成的周报根据实际情况进行修改,产生新的周报。
只要按照如上两个步骤进行处理,工作周报的创建效率将得以大大提高。这个过程让我们想到平时经常进行的两个电脑基本操作:复制和粘贴,快捷键通常为Ctrl + C和Ctrl + V,通过对已有对象的复制和粘贴,我们可以创建大量的相同对象。
如何在一个面向对象系统中实现对象的复制和粘贴呢?不用着急,本章我们介绍的原型模式正为解决此类问题而诞生。
Sunny公司开发人员决定使用原型模式来实现工作周报的快速创建,快速创建工作周报结构图:

WeeklyLog充当具体原型类,Object类充当抽象原型类,clone()方法为原型方法。WeeklyLog类的代码如下所示:
@Data
class WeeklyLog implements Cloneable {
private String name;
private String date;
private String content;
//克隆方法clone(),此处使用Java语言提供的克隆机制, 此时为浅克隆
public WeeklyLog clone() {
Object obj = null;
try {
obj = super.clone();
return (WeeklyLog)obj;
} catch(CloneNotSupportedException e) {
System.out.println("不支持复制!");
return null;
}
}
}
编写如下客户端测试代码:
class Client {
public static void main(String[] args) {
WeeklyLog log_previous = new WeeklyLog(); //创建原型对象
log_previous.setName("张无忌");
log_previous.setDate("第12周");
log_previous.setContent("这周工作很忙,每天加班!");
System.out.println("****周报****");
System.out.println("周次:" + log_previous.getDate());
System.out.println("姓名:" + log_previous.getName());
System.out.println("内容:" + log_previous.getContent());
System.out.println("--------------------------------");
WeeklyLog log_new;
log_new = log_previous.clone(); //调用克隆方法创建克隆对象
log_new.setDate("第13周");
System.out.println("****周报****");
System.out.println("周次:" + log_new.getDate());
System.out.println("姓名:" + log_new.getName());
System.out.println("内容:" + log_new.getContent());
}
}
编译并运行程序,输出结果如下:
****周报****
周次:第12周
姓名:张无忌
内容:这周工作很忙,每天加班!
--------------------------------
****周报****
周次:第13周
姓名:张无忌
内容:这周工作很忙,每天加班!
通过已创建的工作周报可以快速创建新的周报,然后再根据需要修改周报,无须再从头开始创建。原型模式为工作流系统中任务单的快速生成提供了一种解决方案。
思考
如果在Client类的main()函数中增加如下几条语句:
System.out.println(log_previous == log_new);
System.out.println(log_previous.getDate() == log_new.getDate());
System.out.println(log_previous.getName() == log_new.getName());
System.out.println(log_previous.getContent() == log_new.getContent());
预测这些语句的输出结果。
通过引入原型模式,Sunny软件公司OA系统支持工作周报的快速克隆,极大提高了工作周报的编写效率,受到员工的一致好评。
但有员工又发现一个问题,有些工作周报带有附件,例如经理助理“小龙女”的周报通常附有本周项目进展报告汇总表、本周客户反馈信息汇总表等,
如果使用上述原型模式来复制周报,周报虽然可以复制,但是周报的附件并不能复制,这是由于什么原因导致的呢?如何才能实现周报和附件的同时复制呢?我们在本节将讨论如何解决这些问题。
在回答这些问题之前,先介绍一下两种不同的克隆方法,浅克隆(ShallowClone)和深克隆(DeepClone)。
在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。
浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制:

在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。为了让大家更好地理解浅克隆和深克隆的区别,我们首先使用浅克隆来实现工作周报和附件类的复制,其结构:

带附件的周报结构图(浅克隆)
@Data
public class Attachment {
private String name;
public void download()
{
System.out.println("下载附件:" + name);
}
}
@Data
class WeeklyLog implements Cloneable {
private Attachment attachment;
private String name;
private String date;
private String content;
//克隆方法clone(),此处使用Java语言提供的克隆机制, 此时为浅克隆
public WeeklyLog clone() {
Object obj = null;
try {
obj = super.clone();
return (WeeklyLog)obj;
} catch(CloneNotSupportedException e) {
System.out.println("不支持复制!");
return null;
}
}
}
class Client {
public static void main(String[] args) {
WeeklyLog log_previous, log_new;
log_previous = new WeeklyLog(); //创建原型对象
Attachment attachment = new Attachment(); //创建附件对象
log_previous.setAttachment(attachment); //将附件添加到周报中
log_new = log_previous.clone(); //调用克隆方法创建克隆对象
//比较周报
System.out.println("周报是否相同? " + (log_previous == log_new));
//比较附件
System.out.println("附件是否相同? " + (log_previous.getAttachment() == log_new.getAttachment()));
}
}
编译并运行程序,输出结果如下:
周报是否相同? false
附件是否相同? true
由于使用的是浅克隆技术,因此工作周报对象复制成功,通过“==”比较原型对象和克隆对象的内存地址时输出false;
但是比较附件对象的内存地址时输出true,说明它们在内存中是同一个对象。
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。
简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制,如图:

在Java语言中,如果需要实现深克隆,可以通过序列化(Serialization)等方式来实现(也可以用其他序列化方式例如xml序列化 json序列化等。)。
序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。
通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。
需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。
下面我们使用深克隆技术来实现工作周报和附件对象的复制,由于要将附件对象和工作周报对象都写入流中,因此两个类均需要实现Serializable接口,其结构如图:

图7-7 带附件的周报结构图(深克隆)
@Data
public class Attachment implements Serializable {
private String name;
public void download()
{
System.out.println("下载附件:" + name);
}
}
@Data
class WeeklyLog implements Serializable {
private Attachment attachment;
private String name;
private String date;
private String content;
public WeeklyLog deepClone() throws IOException, ClassNotFoundException {
//将对象写入流中
ByteArrayOutputStream bao=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bao);
oos.writeObject(this);
//将对象从流中取出
ByteArrayInputStream bis=new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream ois=new ObjectInputStream(bis);
return (WeeklyLog)ois.readObject();
}
}
class Client {
public static void main(String[] args) {
WeeklyLog log_previous, log_new = null;
log_previous = new WeeklyLog(); //创建原型对象
Attachment attachment = new Attachment(); //创建附件对象
log_previous.setAttachment(attachment); //将附件添加到周报中
try {
log_new = log_previous.deepClone(); //调用深克隆方法创建克隆对象
} catch(Exception e) {
System.err.println("克隆失败!");
}
//比较周报
System.out.println("周报是否相同? " + (log_previous == log_new));
//比较附件
System.out.println("附件是否相同? " + (log_previous.getAttachment() == log_new.getAttachment()));
}
}
编译并运行程序,输出结果如下:
周报是否相同? false
附件是否相同? false
从输出结果可以看出,由于使用了深克隆技术,附件对象也得以复制,因此用“==”比较原型对象的附件和克隆对象的附件时输出结果均为false。深克隆技术实现了原型对象和克隆对象的完全独立,对任意克隆对象的修改都不会给其他对象产生影响,是一种更为理想的克隆实现方式。
Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。
Sunny软件公司在日常办公中有许多公文需要创建、递交和审批,例如《可行性分析报告》、《立项建议书》、《软件需求规格说明书》、《项目进展报告》等,为了提高工作效率,在OA系统中为各类公文均创建了模板,用户可以通过这些模板快速创建新的公文,这些公文模板需要统一进行管理,系统根据用户请求的不同生成不同的新公文。
原型管理器(Prototype Manager)是将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,如果需要某个原型对象的一个克隆,可以通过复制集合中对应的原型对象来获得。
在原型管理器中针对抽象原型类进行编程,以便扩展。其结构图:
下面通过模拟一个简单的公文管理器来介绍原型管理器的设计与实现:
我们使用带原型管理器的原型模式实现公文管理器的设计,其结构如图:

以下是实现该功能的一些核心代码,考虑到代码的可读性,我们对所有的类都进行了简化:
interface OfficialDocument extends Cloneable {
OfficialDocument clone();
void display();
}
//软件需求规格说明书(Software Requirements Specification)类
class SRS implements OfficialDocument {
public OfficialDocument clone() {
OfficialDocument srs = null;
try {
srs = (OfficialDocument)super.clone();
} catch(CloneNotSupportedException e) {
System.out.println("不支持复制!");
}
return srs;
}
public void display() {
System.out.println("《软件需求规格说明书》");
}
}
//可行性分析报告(Feasibility Analysis Report)类
class FAR implements OfficialDocument {
public OfficialDocument clone() {
OfficialDocument far = null;
try {
far = (OfficialDocument)super.clone();
} catch(CloneNotSupportedException e) {
System.out.println("不支持复制!");
}
return far;
}
public void display() {
System.out.println("《可行性分析报告》");
}
}
//原型管理器(使用饿汉式单例实现)
import java.util.Hashtable;
class PrototypeManager {
//定义一个Hashtable,用于存储原型对象
private Hashtable ht=new Hashtable();
private static PrototypeManager pm = new PrototypeManager();
//为Hashtable增加公文对象
private PrototypeManager() {
ht.put("far",new FAR());
ht.put("srs",new SRS());
}
//增加新的公文对象
public void addOfficialDocument(String key,OfficialDocument doc) {
ht.put(key,doc);
}
//通过浅克隆获取新的公文对象
public OfficialDocument getOfficialDocument(String key) {
return ((OfficialDocument)ht.get(key)).clone();
}
public static PrototypeManager getPrototypeManager() {
return pm;
}
}
public class Client {
public static void main(String[] args) {
//获取原型管理器对象
PrototypeManager pm = PrototypeManager.getPrototypeManager();
OfficialDocument doc1,doc2,doc3,doc4;
doc1 = pm.getOfficialDocument("far");
doc1.display();
doc2 = pm.getOfficialDocument("far");
doc2.display();
System.out.println(doc1 == doc2);
doc3 = pm.getOfficialDocument("srs");
doc3.display();
doc4 = pm.getOfficialDocument("srs");
doc4.display();
System.out.println(doc3 == doc4);
}
}
运行结果
《可行性分析报告》
《可行性分析报告》
false
《软件需求规格说明书》
《软件需求规格说明书》
false
原型模式作为一种快速创建大量相同或相似对象的方式,在软件开发中应用较为广泛,很多软件提供的复制(Ctrl + C)和粘贴(Ctrl + V)操作就是原型模式的典型应用,下面对该模式的使用效果和适用情况进行简单的总结。
原型模式的主要优点如下:
当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。
扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。
原型模式的主要缺点如下:
需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。
在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
在以下情况下可以考虑使用原型模式:
创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。
如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。
建造者模式是较为复杂的创建型模式,它将客户端与包含多个组成部分(或部件)的复杂对象的创建过程分离,客户端无须知道复杂对象的内部组成部分与装配方式,只需要知道所需建造者的类型即可。它关注如何一步一步创建一个的复杂对象,不同的具体建造者定义了不同的创建过程,且具体建造者相互独立,增加新的建造者非常方便,无须修改已有代码,系统具有较好的扩展性。
建造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一种对象创建型模式。
建造者模式一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。

● Builder(抽象建造者):它为创建一个产品Product对象的各个部件指定抽象接口,在该接口中一般声明两类方法,一类方法是buildPartX(),它们用于创建复杂对象的各个部件;另一类方法是getResult(),它们用于返回复杂对象。Builder既可以是抽象类,也可以是接口。
●ConcreteBuilder(具体建造者):它实现了Builder接口,实现各个部件的具体构造和装配方法,定义并明确它所创建的复杂对象,也可以提供一个方法返回创建好的复杂产品对象。
●Product(产品角色):它是被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义它的装配过程。
● Director(指挥者):指挥者又称为导演类,它负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其construct()建造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。客户端一般只需要与指挥者进行交互,在客户端确定具体建造者的类型,并实例化具体建造者对象(也可以通过配置文件和反射机制),然后通过指挥者类的构造函数或者Setter方法将该对象传入指挥者类中。
在建造者模式的定义中提到了复杂对象,那么什么是复杂对象?简单来说,复杂对象是指那些包含多个成员属性的对象,这些成员属性也称为部件或零件,如汽车包括方向盘、发动机、轮胎等部件,电子邮件包括发件人、收件人、主题、内容、附件等部件,一个典型的复杂对象类代码示例如下:
@Data
class Product {
private String partA; //定义部件,部件可以是任意类型,包括值类型和引用类型
private String partB;
private String partC;
}
在抽象建造者类中定义了产品的创建方法和返回方法,其典型代码如下:
abstract class Builder {
//创建产品对象
protected Product product=new Product();
public abstract void buildPartA();
public abstract void buildPartB();
public abstract void buildPartC();
//返回产品对象
public Product getResult() {
return product;
}
}
在抽象类Builder中声明了一系列抽象的buildPartX()方法用于创建复杂产品的各个部件,具体建造过程在ConcreteBuilder中实现,此外还提供了工厂方法getResult(),用于返回一个建造好的完整产品。
在ConcreteBuilder中实现了buildPartX()方法,通过调用Product的setPartX()方法可以给产品对象的成员属性设值。不同的具体建造者在实现buildPartX()方法时将有所区别,如setPartX()方法的参数可能不一样,在有些具体建造者类中某些setPartX()方法无须实现(提供一个空实现)。而这些对于客户端来说都无须关心,客户端只需知道具体建造者类型即可。
在建造者模式的结构中还引入了一个指挥者类Director,该类主要有两个作用:一方面它隔离了客户与创建过程;另一方面它控制产品的创建过程,包括某个buildPartX()方法是否被调用以及多个buildPartX()方法调用的先后次序等。指挥者针对抽象建造者编程,客户端只需要知道具体建造者的类型,即可通过指挥者类调用建造者的相关方法,返回一个完整的产品对象。在实际生活中也存在类似指挥者一样的角色,如一个客户去购买电脑,电脑销售人员相当于指挥者,只要客户确定电脑的类型,电脑销售人员可以通知电脑组装人员给客户组装一台电脑。指挥者类的代码示例如下:
class Director {
private Builder builder;
public Director(Builder builder) {
this.builder=builder;
}
public void setBuilder(Builder builder) {
this.builder=builer;
}
//产品构建与组装方法
public Product construct() {
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
return builder.getResult();
}
}
在指挥者类中可以注入一个抽象建造者类型的对象,其核心在于提供了一个建造方法construct(),在该方法中调用了builder对象的构造部件的方法,最后返回一个产品对象。
对于客户端而言,只需关心具体的建造者即可,一般情况下,客户端类代码片段如下所示:
……
**Builder builder = new ConcreteBuilder(); //可通过配置文件实现**
Director director = new Director(builder);
Product product = director.construct();
……
可以通过配置文件来存储具体建造者类ConcreteBuilder的类名,使得更换新的建造者时无须修改源代码,系统扩展更为方便。在客户端代码中,无须关心产品对象的具体组装过程,只需指定具体建造者的类型即可。
建造者模式与抽象工厂模式有点相似,但是建造者模式返回一个完整的复杂产品,而抽象工厂模式返回一系列相关的产品;在抽象工厂模式中,客户端通过选择具体工厂来生成所需对象,而在建造者模式中,客户端通过指定具体建造者类型并指导Director类如何去生成对象,侧重于一步步构造一个复杂对象,然后将结果返回。如果将抽象工厂模式看成一个汽车配件生产厂,生成不同类型的汽车配件,那么建造者模式就是一个汽车组装厂,通过对配件进行组装返回一辆完整的汽车。
Sunny软件公司游戏开发小组决定开发一款名为《Sunny群侠传》的网络游戏,该游戏采用主流的RPG(Role Playing Game,角色扮演游戏)模式,玩家可以在游戏中扮演虚拟世界中的一个特定角色,角色根据不同的游戏情节和统计数据(如力量、魔法、技能等)具有不同的能力,角色也会随着不断升级而拥有更加强大的能力。
作为RPG游戏的一个重要组成部分,需要对游戏角色进行设计,而且随着该游戏的升级将不断增加新的角色。不同类型的游戏角色,其性别、脸型、服装、发型等外部特性都有所差异,例如“天使”拥有美丽的面容和披肩的长发,并身穿一袭白裙;而“恶魔”极其丑陋,留着光头并穿一件刺眼的黑衣。
Sunny公司决定开发一个小工具来创建游戏角色,可以创建不同类型的角色并可以灵活增加新的角色。
Sunny公司的开发人员通过分析发现,游戏角色是一个复杂对象,它包含性别、脸型等多个组成部分,不同的游戏角色其组成部分有所差异,如图:

(注:本图中的游戏角色造型来源于网络,特此说明)
无论是何种造型的游戏角色,它的创建步骤都大同小异,都需要逐步创建其组成部分,再将各组成部分装配成一个完整的游戏角色。如何一步步创建一个包含多个组成部分的复杂对象
Sunny公司开发人员决定使用建造者模式来实现游戏角色的创建,其基本结构如图:

游戏角色创建结构图
ActorController充当指挥者,ActorBuilder充当抽象建造者,HeroBuilder、AngelBuilder和DevilBuilder充当具体建造者,Actor充当复杂产品。完整代码如下所示:
/**
* Actor角色类:复杂产品,考虑到代码的可读性,只列出部分成员属性,且成员属性的类型均为String,真实情况下,有些成员属性的类型需自定义
*/
@Data
class Actor {
private String type; //角色类型
private String sex; //性别
private String face; //脸型
private String costume; //服装
private String hairstyle; //发型
}
/**
* 角色建造器:抽象建造者
*/
abstract class ActorBuilder {
protected Actor actor = new Actor();
public abstract void buildType();
public abstract void buildSex();
public abstract void buildFace();
public abstract void buildCostume();
public abstract void buildHairstyle();
//工厂方法,返回一个完整的游戏角色对象
public Actor createActor() {
return actor;
}
}
/**
* 英雄角色建造器:具体建造者
*/
class HeroBuilder extends ActorBuilder {
public void buildType() {
actor.setType("英雄");
}
public void buildSex() {
actor.setSex("男");
}
public void buildFace() {
actor.setFace("英俊");
}
public void buildCostume() {
actor.setCostume("盔甲");
}
public void buildHairstyle() {
actor.setHairstyle("飘逸");
}
}
/**
* 天使角色建造器:具体建造者
*/
class AngelBuilder extends ActorBuilder {
public void buildType() {
actor.setType("天使");
}
public void buildSex() {
actor.setSex("女");
}
public void buildFace() {
actor.setFace("漂亮");
}
public void buildCostume() {
actor.setCostume("白裙");
}
public void buildHairstyle() {
actor.setHairstyle("披肩长发");
}
}
/**
* 恶魔角色建造器:具体建造者
*/
class DevilBuilder extends ActorBuilder {
public void buildType() {
actor.setType("恶魔");
}
public void buildSex() {
actor.setSex("妖");
}
public void buildFace() {
actor.setFace("丑陋");
}
public void buildCostume() {
actor.setCostume("黑衣");
}
public void buildHairstyle() {
actor.setHairstyle("光头");
}
}
指挥者类ActorController定义了construct()方法,该方法拥有一个抽象建造者ActorBuilder类型的参数,在该方法内部实现了游戏角色对象的逐步构建,代码如下所示:
/**
* 游戏角色创建控制器:指挥者
* 构建者逻辑简单的情况下, 可以将此类省略, 直接再抽象类中来实现一个静态构造方法即可
*/
class ActorController {
//逐步构建复杂产品对象
public Actor construct(ActorBuilder ab) {
Actor actor;
ab.buildType();
ab.buildSex();
ab.buildFace();
ab.buildCostume();
ab.buildHairstyle();
actor=ab.createActor();
return actor;
}
}
客户端代码
public class Client {
public static void main(String[] args) {
ActorBuilder ab = new AngelBuilder(); //针对抽象建造者编程
ActorController ac = new ActorController();
Actor actor;
actor = ac.construct(ab); //通过指挥者创建完整的建造者对象
String type = actor.getType();
System.out.println(type + "的外观:");
System.out.println("性别:" + actor.getSex());
System.out.println("面容:" + actor.getFace());
System.out.println("服装:" + actor.getCostume());
System.out.println("发型:" + actor.getHairstyle());
}
}
输出
天使的外观:
性别:女
面容:漂亮
服装:白裙
发型:披肩长发
在建造者模式中,客户端只需实例化指挥者类,指挥者类针对抽象建造者编程,客户端根据需要传入具体的建造者类型,指挥者将指导具体建造者一步一步构造一个完整的产品(逐步调用具体建造者的buildX()方法),相同的构造过程可以创建完全不同的产品。在游戏角色实例中,如果需要更换角色,只需要修改配置文件,更换具体角色建造者类即可;如果需要增加新角色,可以增加一个新的具体角色建造者类作为抽象角色建造者的子类,再修改配置文件即可,原有代码无须修改,完全符合“开闭原则”。
指挥者类Director在建造者模式中扮演非常重要的作用,简单的Director类用于指导具体建造者如何构建产品,它按一定次序调用Builder的buildPartX()方法,控制调用的先后次序,并向客户端返回一个完整的产品对象。下面我们讨论几种Director的高级应用方式:
在有些情况下,为了简化系统结构,可以将Director和抽象建造者Builder进行合并,在Builder中提供逐步构建复杂产品对象的construct()方法。由于Builder类通常为抽象类,因此可以将construct()方法定义为静态(static)方法。如果将游戏角色设计中的指挥者类ActorController省略,ActorBuilder类的代码修改如下:
删掉ActorController
/**
* 角色建造器:抽象建造者
*/
abstract class ActorBuilder {
protected static Actor actor = new Actor();
public abstract void buildType();
public abstract void buildSex();
public abstract void buildFace();
public abstract void buildCostume();
public abstract void buildHairstyle();
/**
* 使用一个静态方法建造实例。就不需要调度者了
* @param ab
* @return
*/
public static Actor construct(ActorBuilder ab) {
ab.buildType();
ab.buildSex();
ab.buildFace();
ab.buildCostume();
ab.buildHairstyle();
return actor;
}
}
/**
* 去掉了调度者
*/
public class Client {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ActorBuilder ab = new AngelBuilder(); //针对抽象建造者编程
Actor actor = ActorBuilder.construct(ab); //通过指挥者创建完整的建造者对象
String type = actor.getType();
System.out.println(type + "的外观:");
System.out.println("性别:" + actor.getSex());
System.out.println("面容:" + actor.getFace());
System.out.println("服装:" + actor.getCostume());
System.out.println("发型:" + actor.getHairstyle());
}
}
除此之外,还有一种更简单的处理方法,可以将construct()方法的参数去掉,直接在construct()方法中调用buildPartX()方法,代码如下所示:
/**
* 角色建造器:抽象建造者
*/
abstract class ActorBuilder {
protected Actor actor = new Actor();
public abstract void buildType();
public abstract void buildSex();
public abstract void buildFace();
public abstract void buildCostume();
public abstract void buildHairstyle();
/**
* 在省略了调度者的基础上,连construct方法的参数也去掉
*
*/
public Actor construct() {
this.buildType();
this.buildSex();
this.buildFace();
this.buildCostume();
this.buildHairstyle();
return actor;
}
}
/**
* 去掉调度者,连创建方法的创建者也去掉
*/
public class Client {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ActorBuilder ab = new AngelBuilder(); //针对抽象建造者编程
Actor actor = ab.construct(); //通过指挥者创建完整的建造者对象
String type = actor.getType();
System.out.println(type + "的外观:");
System.out.println("性别:" + actor.getSex());
System.out.println("面容:" + actor.getFace());
System.out.println("服装:" + actor.getCostume());
System.out.println("发型:" + actor.getHairstyle());
}
}
此时,construct()方法定义了其他buildPartX()方法调用的次序,为其他方法的执行提供了一个流程模板,这与我们在后面要学习的模板方法模式非常类似。
以上两种对Director类的省略方式都不影响系统的灵活性和可扩展性,同时还简化了系统结构,但加重了抽象建造者类的职责,如果construct()方法较为复杂,待构建产品的组成部分较多,建议还是将construct()方法单独封装在Director中,这样做更符合“单一职责原则”。
建造者模式除了逐步构建一个复杂产品对象外,还可以通过Director类来更加精细地控制产品的创建过程,例如增加一类称之为钩子方法(HookMethod)的特殊方法来控制是否对某个buildPartX()的调用。
钩子方法的返回类型通常为boolean类型,方法名一般为isXXX(),钩子方法定义在抽象建造者类中。例如我们可以在游戏角色的抽象建造者类ActorBuilder中定义一个方法isBareheaded(),用于判断某个角色是否为“光头(Bareheaded)”,在ActorBuilder为之提供一个默认实现,其返回值为false,代码如下所示:
/**
* 角色建造器:抽象建造者
*/
abstract class ActorBuilder {
protected Actor actor = new Actor();
public abstract void buildType();
public abstract void buildSex();
public abstract void buildFace();
public abstract void buildCostume();
public abstract void buildHairstyle();
//工厂方法,返回一个完整的游戏角色对象
public Actor createActor() {
return actor;
}
public boolean isBareheaded() {
return false;
}
}
如果某个角色无须构建头发部件,例如“恶魔(Devil)”,则对应的具体建造器DevilBuilder将覆盖isBareheaded()方法,并将返回值改为true,代码如下所示:
/**
* 恶魔角色建造器:具体建造者
*/
class DevilBuilder extends ActorBuilder {
public void buildType() {
actor.setType("恶魔");
}
public void buildSex() {
actor.setSex("妖");
}
public void buildFace() {
actor.setFace("丑陋");
}
public void buildCostume() {
actor.setCostume("黑衣");
}
public void buildHairstyle() {
actor.setHairstyle("光头");
}
//覆盖钩子方法
public boolean isBareheaded() {
return true;
}
}
此时,指挥者类ActorController的代码修改如下:
/**
* 游戏角色创建控制器:指挥者
* 构建者逻辑简单的情况下, 可以将此类省略, 直接再抽象类中来实现一个静态构造方法即可
*/
class ActorController {
//逐步构建复杂产品对象
public Actor construct(ActorBuilder ab) {
Actor actor;
ab.buildType();
ab.buildSex();
ab.buildFace();
ab.buildCostume();
//通过钩子方法来控制产品的构建
if(!ab.isBareheaded()) {
ab. buildHairstyle();
}
actor=ab.createActor();
return actor;
}
}
当在客户端代码中指定具体建造者类型并通过指挥者来实现产品的逐步构建时,将调用钩子方法isBareheaded()来判断游戏角色是否有头发,如果isBareheaded()方法返回true,即没有头发,则跳过构建发型的方法buildHairstyle();否则将执行buildHairstyle()方法。通过引入钩子方法,我们可以在Director中对复杂产品的构建进行精细的控制,不仅指定buildPartX()方法的执行顺序,还可以控制是否需要执行某个buildPartX()方法。
客户端测试
/**
* 标准的建造者模式实现,添加钩子方法控制构建过程
*/
public class Client {
public static void main(String[] args) {
ActorBuilder ab = new DevilBuilder(); //恶魔构建者重写来了钩子方法,是一个光头。返回true。所以发型返回了null
ActorController ac = new ActorController();
Actor actor = ac.construct(ab); //通过指挥者创建完整的建造者对象
String type = actor.getType();
System.out.println(type + "的外观:");
System.out.println("性别:" + actor.getSex());
System.out.println("面容:" + actor.getFace());
System.out.println("服装:" + actor.getCostume());
System.out.println("发型:" + actor.getHairstyle());
}
}
执行结果
恶魔的外观:
性别:妖
面容:丑陋
服装:黑衣
发型:null
建造者模式的核心在于如何一步步构建一个包含多个组成部件的完整对象,使用相同的构建过程构建不同的产品,在软件开发中,如果我们需要创建复杂对象并希望系统具备很好的灵活性和可扩展性可以考虑使用建造者模式。
建造者模式的主要优点如下:
在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。由于指挥者类针对抽象建造者编程,增加新的具体建造者无须修改原有类库的代码,系统扩展方便,符合“开闭原则”
可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
建造者模式的主要缺点如下:
建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,例如很多组成部分都不相同,不适合使用建造者模式,因此其使用范围受到一定的限制。
如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,增加系统的理解难度和运行成本。
在以下情况下可以考虑使用建造者模式:
需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
对象的创建过程独立于创建该对象的类。在建造者模式中通过引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类和客户类中。
隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
与电源适配器相似,在适配器模式中引入了一个被称为适配器(Adapter)的包装类,而它所包装的对象称为适配者(Adaptee),即被适配的类。
适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。
也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。
因此,适配器让那些由于接口不兼容而不能交互的类可以一起工作。
适配器模式可以将一个类的接口和另一个类的接口匹配起来,而无须修改原来的适配者接口和抽象目标类接口。
适配器模式(Adapter Pattern):将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
【注:在适配器模式定义中所提及的接口是指广义的接口,它可以表示一个方法或者方法的集合。】
在适配器模式中,我们通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作。根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。
在实际开发中,对象适配器的使用频率更高

● **Target(目标抽象类):**目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。
● **Adapter(适配器类):**适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。
● Adaptee**(适配者类):**适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。
根据对象适配器模式结构图,在对象适配器中,客户端需要调用request()方法,而适配者类Adaptee没有该方法,但是它所提供的specificRequest()方法却是客户端所需要的。为了使客户端能够使用适配者类,需要提供一个包装类Adapter,即适配器类。这个包装类包装了一个适配者的实例,从而将客户端与适配者衔接起来,在适配器的request()方法中调用适配者的specificRequest()方法。因为适配器类与适配者类是关联关系(也可称之为委派关系),所以这种适配器模式称为对象适配器模式。
class Adapter extends Target {
private Adaptee adaptee; //维持一个对适配者对象的引用
public Adapter(Adaptee adaptee) {
this.adaptee=adaptee;
}
public void request() {
adaptee.specificRequest(); //转发调用
}
}
Sunny软件公司在很久以前曾开发了一个算法库,里面包含了一些常用的算法,例如排序算法和查找算法,在进行各类软件开发时经常需要重用该算法库中的算法。
在为某学校开发教务管理系统时,开发人员发现需要对学生成绩进行排序和查找,该系统的设计人员已经开发了一个成绩操作接口ScoreOperation,在该接口中声明了排序方法sort(int[]) 和查找方法search(int[], int),为了提高排序和查找的效率,开发人员决定重用算法库中的快速排序算法类QuickSort和二分查找算法类BinarySearch,其中QuickSort的quickSort(int[])方法实现了快速排序,BinarySearch 的binarySearch (int[], int)方法实现了二分查找。
由于某些原因,现在Sunny公司开发人员已经找不到该算法库的源代码,无法直接通过复制和粘贴操作来重用其中的代码;部分开发人员已经针对ScoreOperation接口编程,如果再要求对该接口进行修改或要求大家直接使用QuickSort类和BinarySearch类将导致大量代码需要修改。
Sunny软件公司开发人员面对这个没有源码的算法库,遇到一个幸福而又烦恼的问题:如何在既不修改现有接口又不需要任何算法库代码的基础上能够实现算法库的重用?
通过分析,我们不难得知,现在Sunny软件公司面对的问题有点类似本章最开始所提到的电压问题,成绩操作接口ScoreOperation好比只支持20V电压的笔记本,而算法库好比220V的家庭用电,这两部分都没有办法再进行修改,而且它们原本是两个完全不相关的结构,如图所示:

现在我们需要ScoreOperation接口能够和已有算法库一起工作,让它们在同一个系统中能够兼容,最好的实现方法是增加一个类似电源适配器一样的适配器角色,通过适配器来协调这两个原本不兼容的结构。如何在软件开发中设计和实现适配器是本章我们将要解决的核心问题,下面就让我们正式开始学习这种用于解决不兼容结构问题的适配器模式。
Sunny软件公司开发人员决定使用适配器模式来重用算法库中的算法,其基本结构如图所示:

ScoreOperation接口充当抽象目标,QuickSort和BinarySearch类充当适配者,OperationAdapter充当适配器。完整代码如下所示:
/**
* 抽象成绩操作类:目标接口
*/
interface ScoreOperation {
int[] sort(int[] array); //成绩排序
int search(int[] array, int key); //成绩查找
}
/**
* 快速排序类:适配者
*/
public class QuickSort {
public int[] quickSort(int array[]) {
sort(array,0,array.length-1);
return array;
}
public void sort(int array[],int p, int r) {
int q=0;
if(p<r) {
q=partition(array,p,r);
sort(array,p,q-1);
sort(array,q+1,r);
}
}
public int partition(int[] a, int p, int r) {
int x=a[r];
int j=p-1;
for (int i=p;i<=r-1;i++) {
if (a[i]<=x) {
j++;
swap(a,j,i);
}
}
swap(a,j+1,r);
return j+1;
}
public void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
/**
* 二分查找类:适配者
*/
public class BinarySearch {
public int binarySearch(int array[], int key) {
int low = 0;
int high = array.length - 1;
while (low <= high) {
int mid = (low + high) / 2;
int midVal = array[mid];
if (midVal < key) {
low = mid + 1;
} else if (midVal > key) {
high = mid - 1;
} else {
return 1; //找到元素返回1
}
}
return -1; //未找到元素返回-1
}
}
/**
* 操作适配器:适配器
*/
class OperationAdapter implements ScoreOperation {
private QuickSort sortObj; //定义适配者QuickSort对象
private BinarySearch searchObj; //定义适配者BinarySearch对象
public OperationAdapter() {
sortObj = new QuickSort();
searchObj = new BinarySearch();
}
public int[] sort(int array[]) {
return sortObj.quickSort(array); //调用适配者类QuickSort的排序方法
}
public int search(int array[],int key) {
return searchObj.binarySearch(array,key); //调用适配者类BinarySearch的查找方法
}
}
编写如下客户端测试代码:
class Client {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
ScoreOperation operation = new OperationAdapter(); //针对抽象目标接口编程
int[] scores = {84,76,50,69,90,91,88,96}; //定义成绩数组
int[] result;
int score;
System.out.println("成绩排序结果:");
result = operation.sort(scores);
//遍历输出成绩
for(int i : scores) {
System.out.print(i + ",");
}
System.out.println();
System.out.println("查找成绩90:");
score = operation.search(result,90);
if (score != -1) {
System.out.println("找到成绩90。");
}
else {
System.out.println("没有找到成绩90。");
}
System.out.println("查找成绩92:");
score = operation.search(result,92);
if (score != -1) {
System.out.println("找到成绩92。");
}
else {
System.out.println("没有找到成绩92。");
}
}
}
编译并运行程序,输出结果如下:
成绩排序结果:
成绩排序结果:
50,69,76,84,88,90,91,96,
查找成绩90:
找到成绩90。
查找成绩92:
没有找到成绩92。
如果需要使用其他排序算法类和查找算法类,可以增加一个新的适配器类,使用新的适配器来适配新的算法,原有代码无须修改。
除了对象适配器模式之外,适配器模式还有一种形式,那就是类适配器模式。
类适配器模式和对象适配器模式最大的区别在于适配器和适配者之间的关系不同,对象适配器模式中适配器和适配者之间是关联关系,而类适配器模式中适配器和适配者是继承关系,
类适配器模式结构:

根据类适配器模式结构图,适配器类实现了抽象目标类接口Target,并继承了适配者类,在适配器类的request()方法中调用所继承的适配者类的specificRequest()方法,实现了适配。
典型的类适配器代码如下所示:
class Adapter extends Adaptee implements Target {
public void request() {
specificRequest();
}
}
由于Java、C#等语言不支持多重类继承,因此类适配器的使用受到很多限制,例如如果目标抽象类Target不是接口,而是一个类,就无法使用类适配器;
此外,如果适配者Adaptee为最终(Final)类,也无法使用类适配器。
在Java等面向对象编程语言中,大部分情况下我们使用的是对象适配器,类适配器较少使用。
在对象适配器的使用过程中,如果在适配器中同时包含对目标类和适配者类的引用,适配者可以通过它调用目标类中的方法,目标类也可以通过它调用适配者类中的方法,那么该适配器就是一个双向适配器,其结构示意图:

双向适配器的实现较为复杂,其典型代码如下所示:
class Adapter implements Target,Adaptee {
//同时维持对抽象目标类和适配者的引用
private Target target;
private Adaptee adaptee;
public Adapter(Target target) {
this.target = target;
}
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
public void request() {
adaptee.specificRequest();
}
public void specificRequest() {
target.request();
}
}
在实际开发中,我们很少使用双向适配器。
缺省适配器模式是适配器模式的一种变体,其应用也较为广泛。缺省适配器模式的定义如下:
缺省适配器模式(Default Adapter Pattern):当不需要实现一个接口所提供的所有方法时,可先设计一个抽象类实现该接口,并为接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可以选择性地覆盖父类的某些方法来实现需求,它适用于不想使用一个接口中的所有方法的情况,又称为单接口适配器模式。
缺省适配器模式结构所示:

在缺省适配器模式中,包含如下三个角色:
**● ServiceInterface(适配者接口):**它是一个接口,通常在该接口中声明了大量的方法。
**● AbstractServiceClass(缺省适配器类):**它是缺省适配器模式的核心类,使用空方法的形式实现了在ServiceInterface接口中声明的方法。通常将它定义为抽象类,因为对它进行实例化没有任何意义。
**● ConcreteServiceClass(具体业务类):**它是缺省适配器类的子类,在没有引入适配器之前,它需要实现适配者接口,因此需要实现在适配者接口中定义的所有方法,而对于一些无须使用的方法也不得不提供空实现。在有了缺省适配器之后,可以直接继承该适配器类,根据需要有选择性地覆盖在适配器类中定义的方法。
在JDK类库的事件处理包java.awt.event中广泛使用了缺省适配器模式,如WindowAdapter、KeyAdapter、MouseAdapter等。
下面我们以处理窗口事件为例来进行说明:在Java语言中,一般我们可以使用两种方式来实现窗口事件处理类,一种是通过实现WindowListener接口,另一种是通过继承WindowAdapter适配器类。
如果是使用第一种方式,直接实现WindowListener接口,事件处理类需要实现在该接口中定义的七个方法,而对于大部分需求可能只需要实现一两个方法,其他方法都无须实现,但由于语言特性我们不得不为其他方法也提供一个简单的实现(通常是空实现),这给使用带来了麻烦。
而使用缺省适配器模式就可以很好地解决这一问题,在JDK中提供了一个适配器类WindowAdapter来实现WindowListener接口,该适配器类为接口中的每一个方法都提供了一个空实现,此时事件处理类可以继承WindowAdapter类,而无须再为接口中的每个方法都提供实现。WindowListener和WindowAdapter结构图:
适配器模式将现有接口转化为客户类所期望的接口,实现了对现有类的复用,它是一种使用频率非常高的设计模式,在软件开发中得以广泛应用,在Spring等开源框架、驱动程序设计(如JDBC中的数据库驱动程序)中也使用了适配器模式。
将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。
具体来说,类适配器模式还有如下优点:
由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。
对象适配器模式还有如下优点:
一个对象适配器可以把多个不同的适配者适配到同一个目标;
可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也可通过该适配器进行适配。
对于Java、C#等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者;
适配者类不能为最终类,如在Java中不能为final类,C#中不能为sealed类;
在Java、C#等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。
对象适配器模式的缺点如下:
与类适配器模式相比,要在适配器中置换适配者类的某些方法比较麻烦。如果一定要置换掉适配者类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。
在以下情况下可以考虑使用适配器模式:
系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
桥接模式是一种很实用的结构型设计模式,如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。
与多层继承方案不同,它将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系类似一条连接两个独立继承结构的桥,故名桥接模式。
桥接模式用一种巧妙的方式处理多层继承存在的问题,用抽象关联取代了传统的多层继承,将类之间的静态继承关系转换为动态的对象组合关系,使得系统更加灵活,并易于扩展,同时有效控制了系统中类的个数。
桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。
桥接模式的结构与其名称一样,存在一条连接两个继承等级结构的桥。

●Abstraction(抽象类):用于定义抽象类的接口,它一般是抽象类而不是接口,其中定义了一个Implementor(实现类接口)类型的对象并可以维护该对象,它与Implementor之间具有关联关系,它既可以包含抽象业务方法,也可以包含具体业务方法。
●RefinedAbstraction(扩充抽象类):扩充由Abstraction定义的接口,通常情况下它不再是抽象类而是具体类,它实现了在Abstraction中声明的抽象业务方法,在RefinedAbstraction中可以调用在Implementor中定义的业务方法。
●Implementor(实现类接口):定义实现类的接口,这个接口不一定要与Abstraction的接口完全一致,事实上这两个接口可以完全不同,一般而言,Implementor接口仅提供基本操作,而Abstraction定义的接口可能会做更多更复杂的操作。Implementor接口对这些基本操作进行了声明,而具体实现交给其子类。通过关联关系,在Abstraction中不仅拥有自己的方法,还可以调用到Implementor中定义的方法,使用关联关系来替代继承关系。
●ConcreteImplementor(具体实现类):具体实现Implementor接口,在不同的ConcreteImplementor中提供基本操作的不同实现,在程序运行时,ConcreteImplementor对象将替换其父类对象,提供给抽象类具体的业务操作方法。
桥接模式是一个非常有用的模式,在桥接模式中体现了很多面向对象设计原则的思想,包括“单一职责原则”、“开闭原则”、“合成复用原则”、“里氏代换原则”、“依赖倒转原则”等。熟悉桥接模式有助于我们深入理解这些设计原则,也有助于我们形成正确的设计思想和培养良好的设计风格。
在使用桥接模式时,我们首先应该识别出一个类所具有的两个独立变化的维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。
通常情况下,我们将具有两个独立变化维度的类的一些普通业务方法和与之关系最密切的维度设计为“抽象类”层次结构(抽象部分),而将另一个维度设计为“实现类”层次结构(实现部分)。
例如:对于毛笔而言,由于型号是其固有的维度,因此可以设计一个抽象的毛笔类,在该类中声明并部分实现毛笔的业务方法,而将各种型号的毛笔作为其子类;
颜色是毛笔的另一个维度,由于它与毛笔之间存在一种“设置”的关系,因此我们可以提供一个抽象的颜色接口,而将具体的颜色作为实现该接口的子类。
在此,型号可认为是毛笔的抽象部分,而颜色是毛笔的实现部分,结构示意图:

如果需要增加一种新型号的毛笔,只需扩展左侧的“抽象部分”,增加一个新的扩充抽象类;如果需要增加一种新的颜色,只需扩展右侧的“实现部分”,增加一个新的具体实现类。扩展非常方便,无须修改已有代码,且不会导致类的数目增长过快。
在具体编码实现时,由于在桥接模式中存在两个独立变化的维度,为了使两者之间耦合度降低,首先需要针对两个不同的维度提取抽象类和实现类接口,并建立一个抽象关联关系。对于“实现部分”维度,典型的实现类接口代码如下所示:
interface Implementor {
public void operationImpl();
}
在实现Implementor接口的子类中实现了在该接口中声明的方法,用于定义与该维度相对应的一些具体方法。
对于另一“抽象部分”维度而言,其典型的抽象类代码如下所示:
abstract class Abstraction {
protected Implementor impl; //定义实现类接口对象
public void setImpl(Implementor impl) {
this.impl=impl;
}
public abstract void operation(); //声明抽象业务方法
}
在抽象类Abstraction中定义了一个实现类接口类型的成员对象impl,再通过注入的方式给该对象赋值,一般将该对象的可见性定义为protected,以便在其子类中访问Implementor的方法,其子类一般称为扩充抽象类或细化抽象类(RefinedAbstraction),典型的RefinedAbstraction类代码如下所示:
class RefinedAbstraction extends Abstraction {
public void operation() {
//业务代码
impl.operationImpl(); //调用实现类的方法
//业务代码
}
}
对于客户端而言,可以针对两个维度的抽象层编程,在程序运行时再动态确定两个维度的子类,动态组合对象,将两个独立变化的维度完全解耦,以便能够灵活地扩充任一维度而对另一维度不造成任何影响。
思考
如果系统中存在两个以上的变化维度,是否可以使用桥接模式进行处理?如果可以,系统该如何设计?
Sunny软件公司欲开发一个跨平台图像浏览系统,要求该系统能够显示BMP、JPG、GIF、PNG等多种格式的文件,并且能够在Windows、Linux、Unix等多个操作系统上运行。
系统首先将各种格式的文件解析为像素矩阵(Matrix),然后将像素矩阵显示在屏幕上,在不同的操作系统中可以调用不同的绘制函数来绘制像素矩阵。
系统需具有较好的扩展性以支持新的文件格式和操作系统。
Sunny软件公司的开发人员针对上述要求,提出了一个初始设计方案,其基本结构:

初始设计方案中,使用了一种多层继承结构,Image是抽象父类,而每一种类型的图像类,如BMPImage、JPGImage等作为其直接子类,不同的图像文件格式具有不同的解析方法,可以得到不同的像素矩阵;
由于每一种图像又需要在不同的操作系统中显示,不同的操作系统在屏幕上显示像素矩阵有所差异,因此需要为不同的图像类再提供一组在不同操作系统显示的子类,如为BMPImage提供三个子类BMPWindowsImp、BMPLinuxImp和BMPUnixImp,分别用于在Windows、Linux和Unix三个不同的操作系统下显示图像。
我们现在对该设计方案进行分析,发现存在如下两个主要问题:
由于采用了多层继承结构,导致系统中类的个数急剧增加,图10-1中,在各种图像的操作系统实现层提供了12个具体类,加上各级抽象层的类,系统中类的总个数达到了17个,在该设计方案中,具体层的类的个数 = 所支持的图像文件格式数×所支持的操作系统数。
系统扩展麻烦,由于每一个具体类既包含图像文件格式信息,又包含操作系统信息,因此无论是增加新的图像文件格式还是增加新的操作系统,都需要增加大量的具体类,例如在图10-1中增加一种新的图像文件格式TIF,则需要增加3个具体类来实现该格式图像在3种不同操作系统的显示;如果增加一个新的操作系统Mac OS,为了在该操作系统下能够显示各种类型的图像,需要增加4个具体类。这将导致系统变得非常庞大,增加运行和维护开销。
如何解决这两个问题?我们通过分析可得知,该系统存在两个独立变化的维度:图像文件格式和操作系统,如图:

如何将各种不同类型的图像文件解析为像素矩阵与图像文件格式本身相关,而如何在屏幕上显示像素矩阵则仅与操作系统相关。
因初始方案所示结构将这两种职责集中在一个类中,导致系统扩展麻烦,从类的设计角度分析,具体类BMPWindowsImp、BMPLinuxImp和BMPUnixImp等违反了“单一职责原则”,因为不止一个引起它们变化的原因,它们将图像文件解析和像素矩阵显示这两种完全不同的职责融合在一起,任意一个职责发生改变都需要修改它们,系统扩展困难。
如何改进?我们的方案是将图像文件格式(对应图像格式的解析)与操作系统(对应像素矩阵的显示)两个维度分离,使得它们可以独立变化,增加新的图像文件格式或者操作系统时都对另一个维度不造成任何影响。看到这里,大家可能会问,到底如何在软件中实现将两个维度分离呢?不用着急,本章我将为大家详细介绍一种用于处理多维度变化的设计模式——桥接模式。
为了减少所需生成的子类数目,实现将操作系统和图像文件格式两个维度分离,使它们可以独立改变,Sunny公司开发人员使用桥接模式来重构跨平台图像浏览系统的设计,其基本结构:

Image充当抽象类,其子类JPGImage、PNGImage、BMPImage和GIFImage充当扩充抽象类;ImageImp充当实现类接口,其子类WindowsImp、LinuxImp和UnixImp充当具体实现类。完整代码如下所示:
//像素矩阵类:辅助类,各种格式的文件最终都被转化为像素矩阵,不同的操作系统提供不同的方式显示像素矩阵
class Matrix {
//此处代码省略
}
//抽象图像类:抽象类
abstract class Image {
protected ImageImp imp;
public void setImageImp(ImageImp imp) {
this.imp = imp;
}
public abstract void parseFile(String fileName);
}
//抽象操作系统实现类:实现类接口
interface ImageImp {
void doPaint(Matrix m); //显示像素矩阵m
}
//Windows操作系统实现类:具体实现类
class WindowsImp implements ImageImp {
public void doPaint(Matrix m) {
//调用Windows系统的绘制函数绘制像素矩阵
System.out.print("在Windows操作系统中显示图像:");
}
}
//Linux操作系统实现类:具体实现类
class LinuxImp implements ImageImp {
public void doPaint(Matrix m) {
//调用Linux系统的绘制函数绘制像素矩阵
System.out.print("在Linux操作系统中显示图像:");
}
}
//Unix操作系统实现类:具体实现类
class UnixImp implements ImageImp {
public void doPaint(Matrix m) {
//调用Unix系统的绘制函数绘制像素矩阵
System.out.print("在Unix操作系统中显示图像:");
}
}
//JPG格式图像:扩充抽象类
class JPGImage extends Image {
public void parseFile(String fileName) {
//模拟解析JPG文件并获得一个像素矩阵对象m;
Matrix m = new Matrix();
imp.doPaint(m);
System.out.println(fileName + ",格式为JPG。");
}
}
//PNG格式图像:扩充抽象类
class PNGImage extends Image {
public void parseFile(String fileName) {
//模拟解析PNG文件并获得一个像素矩阵对象m;
Matrix m = new Matrix();
imp.doPaint(m);
System.out.println(fileName + ",格式为PNG。");
}
}
//BMP格式图像:扩充抽象类
class BMPImage extends Image {
public void parseFile(String fileName) {
//模拟解析BMP文件并获得一个像素矩阵对象m;
Matrix m = new Matrix();
imp.doPaint(m);
System.out.println(fileName + ",格式为BMP。");
}
}
//GIF格式图像:扩充抽象类
class GIFImage extends Image {
public void parseFile(String fileName) {
//模拟解析GIF文件并获得一个像素矩阵对象m;
Matrix m = new Matrix();
imp.doPaint(m);
System.out.println(fileName + ",格式为GIF。");
}
}
编写如下客户端测试代码:
public class Client {
public static void main(String[] args) {
Image image = new JPGImage();
ImageImp imp = new LinuxImp();
image.setImageImp(imp);
image.parseFile("小龙女");
}
}
编译并运行程序,输出结果如下:
在Linux操作系统中显示图像:小龙女,格式为JPG。
在实际使用时,可以通过分析图像文件格式后缀名来确定具体的文件格式,在程序运行时获取操作系统信息来确定操作系统类型。
当增加新的图像文件格式或者操作系统时,原有系统无须做任何修改,只需增加一个对应的扩充抽象类或具体实现类即可,系统具有较好的可扩展性,完全符合“开闭原则”。
在软件开发中,适配器模式通常可以与桥接模式联合使用。
适配器模式可以解决两个已有接口间不兼容问题,在这种情况下被适配的类往往是一个黑盒子,有时候我们不想也不能改变这个被适配的类,也不能控制其扩展。
适配器模式通常用于现有系统与第三方产品功能的集成,采用增加适配器的方式将第三方类集成到系统中。
桥接模式则不同,用户可以通过接口继承或类继承的方式来对系统进行扩展。
桥接模式和适配器模式用于设计的不同阶段,桥接模式用于系统的初步设计,对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化;
而在初步设计完成之后,当发现系统与已有类无法协同工作时,可以采用适配器模式。
但有时候在设计初期也需要考虑适配器模式,特别是那些涉及到大量第三方应用接口的情况。
下面通过一个实例来说明适配器模式和桥接模式的联合使用:
在某系统的报表处理模块中,需要将报表显示和数据采集分开,系统可以有多种报表显示方式也可以有多种数据采集方式,如可以从文本文件中读取数据,也可以从数据库中读取数据,还可以从Excel文件中获取数据。
如果需要从Excel文件中获取数据,则需要调用与Excel相关的API,而这个API是现有系统所不具备的,该API由厂商提供。使用适配器模式和桥接模式设计该模块。
在设计过程中,由于存在报表显示和数据采集两个独立变化的维度,因此可以使用桥接模式进行初步设计;
为了使用Excel相关的API来进行数据采集则需要使用适配器模式。系统的完整设计中需要将两个模式联用,如图:

桥接模式是设计Java虚拟机和实现JDBC等驱动程序的核心模式之一,应用较为广泛。
在软件开发中如果一个类或一个系统有多个变化维度时,都可以尝试使用桥接模式对其进行设计。
桥接模式为多维度变化的系统提供了一套完整的解决方案,并且降低了系统的复杂度。
分离抽象接口及其实现部分。桥接模式使用“对象间的关联关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,也就是说抽象和实现不再在同一个继承层次结构中,而是“子类化”它们,使它们各自都具有自己的子类,以便任何组合子类,从而获得多维度组合对象。
在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。
桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。
桥接模式的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。
桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性,如何正确识别两个独立维度也需要一定的经验积累。
在以下情况下可以考虑使用桥接模式:
如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。
“抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
对于树形结构,当容器对象(如文件夹)的某一个方法被调用时,将遍历整个树形结构,寻找也包含这个方法的成员对象(可以是容器对象,也可以是叶子对象)并调用执行,牵一而动百,其中使用了递归调用的机制来对整个结构进行处理。由于容器对象和叶子对象在功能上的区别,在使用这些对象的代码中必须有区别地对待容器对象和叶子对象,而实际上大多数情况下我们希望一致地处理它们,因为对于这些对象的区别对待将会使得程序非常复杂。组合模式为解决此类问题而诞生,它可以让叶子对象和容器对象的使用具有一致性。
组合模式(Composite Pattern):组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,组合模式又可以称为“整体—部分”(Part-Whole)模式,它是一种对象结构型模式。
在组合模式中引入了抽象构件类Component,它是所有容器类和叶子类的公共父类,客户端针对Component进行编程。

**● Component(抽象构件):**它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等。
**● Leaf(叶子构件):**它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。
**● Composite(容器构件):**它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。
**组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。**同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。
如果不使用组合模式,客户端代码将过多地依赖于容器对象复杂的内部实现结构,容器对象内部实现结构的变化将引起客户代码的频繁变化,带来了代码维护复杂、可扩展性差等弊端。组合模式的引入将在一定程度上解决这些问题。
下面通过简单的示例代码来分析组合模式的各个角色的用途和实现。对于组合模式中的抽象构件角色。
abstract class Component {
public abstract void add(Component c); //增加成员
public abstract void remove(Component c); //删除成员
public abstract Component getChild(int i); //获取成员
public abstract void operation(); //业务方法
}
一般将抽象构件类设计为接口或抽象类,将所有子类共有方法的声明和实现放在抽象构件类中。对于客户端而言,将针对抽象构件编程,而无须关心其具体子类是容器构件还是叶子构件。
如果继承抽象构件的是叶子构件,则其典型代码如下所示:
class Leaf extends Component {
public void add(Component c) {
//异常处理或错误提示
}
public void remove(Component c) {
//异常处理或错误提示
}
public Component getChild(int i) {
//异常处理或错误提示
return null;
}
public void operation() {
//叶子构件具体业务方法的实现
}
}
作为抽象构件类的子类,在叶子构件中需要实现在抽象构件类中声明的所有方法,包括业务方法以及管理和访问子构件的方法,但是叶子构件不能再包含子构件,因此在叶子构件中实现子构件管理和访问方法时需要提供异常处理或错误提示。当然,这无疑会给叶子构件的实现带来麻烦。
如果继承抽象构件的是容器构件,则其典型代码如下所示:
class Composite extends Component {
private ArrayList<Component> list = new ArrayList<Component>();
public void add(Component c) {
list.add(c);
}
public void remove(Component c) {
list.remove(c);
}
public Component getChild(int i) {
return (Component)list.get(i);
}
public void operation() {
//容器构件具体业务方法的实现
//递归调用成员构件的业务方法
for(Object obj:list) {
((Component)obj).operation();
}
}
}
在容器构件中实现了在抽象构件中声明的所有方法,既包括业务方法,也包括用于访问和管理成员子构件的方法,如add()、remove()和getChild()等方法。需要注意的是在实现具体业务方法时,由于容器构件充当的是容器角色,包含成员构件,因此它将调用其成员构件的业务方法。在组合模式结构中,由于容器构件中仍然可以包含容器构件,因此在对容器构件进行处理时需要使用递归算法,即在容器构件的operation()方法中递归调用其成员构件的operation()方法。
树形结构在软件中随处可见,例如操作系统中的目录结构、应用软件中的菜单、办公系统中的公司组织结构等等,如何运用面向对象的方式来处理这种树形结构是组合模式需要解决的问题,组合模式通过一种巧妙的设计方案使得用户可以一致性地处理整个树形结构或者树形结构的一部分,也可以一致性地处理树形结构中的叶子节点(不包含子节点的节点)和容器节点(包含子节点的节点)。下面将学习这种用于处理树形结构的组合模式。
Sunny软件公司欲开发一个杀毒(AntiVirus)软件,该软件既可以对某个文件夹(Folder)杀毒,也可以对某个指定的文件(File)进行杀毒。
该杀毒软件还可以根据各类文件的特点,为不同类型的文件提供不同的杀毒方式,例如图像文件(ImageFile)和文本文件(TextFile)的杀毒方式就有所差异。
现需要提供该杀毒软件的整体框架设计方案。
在介绍Sunny公司开发人员提出的初始解决方案之前,我们先来分析一下操作系统中的文件目录结构,例如在Windows操作系统中,存在目录结构:

上图可以简化为下图所示的树形目录结构:

我们可以看出,在图中包含文件(灰色节点)和文件夹(白色节点)两类不同的元素,其中在文件夹中可以包含文件,还可以继续包含子文件夹,但是在文件中不能再包含子文件或者子文件夹。
在此,我们可以称文件夹为容器(Container),而不同类型的各种文件是其成员,也称为叶子(Leaf),一个文件夹也可以作为另一个更大的文件夹的成员。
如果我们现在要对某一个文件夹进行操作,如查找文件,那么需要对指定的文件夹进行遍历,如果存在子文件夹则打开其子文件夹继续遍历,如果是文件则判断之后返回查找结果。
Sunny软件公司的开发人员通过分析,决定使用面向对象的方式来实现对文件和文件夹的操作,定义了如下图像文件类ImageFile、文本文件类TextFile和文件夹类Folder:
//为了突出核心框架代码,我们对杀毒过程的实现进行了大量简化
import java.util.*;
//图像文件类
class ImageFile {
private String name;
public ImageFile(String name) {
this.name = name;
}
public void killVirus() {
//简化代码,模拟杀毒
System.out.println("----对图像文件'" + name + "'进行杀毒");
}
}
//文本文件类
class TextFile {
private String name;
public TextFile(String name) {
this.name = name;
}
public void killVirus() {
//简化代码,模拟杀毒
System.out.println("----对文本文件'" + name + "'进行杀毒");
}
}
//文件夹类
class Folder {
private String name;
//定义集合folderList,用于存储Folder类型的成员
private ArrayList<Folder> folderList = new ArrayList<Folder>();
//定义集合imageList,用于存储ImageFile类型的成员
private ArrayList<ImageFile> imageList = new ArrayList<ImageFile>();
//定义集合textList,用于存储TextFile类型的成员
private ArrayList<TextFile> textList = new ArrayList<TextFile>();
public Folder(String name) {
this.name = name;
}
//增加新的Folder类型的成员
public void addFolder(Folder f) {
folderList.add(f);
}
//增加新的ImageFile类型的成员
public void addImageFile(ImageFile image) {
imageList.add(image);
}
//增加新的TextFile类型的成员
public void addTextFile(TextFile text) {
textList.add(text);
}
//需提供三个不同的方法removeFolder()、removeImageFile()和removeTextFile()来删除成员,代码省略
//需提供三个不同的方法getChildFolder(int i)、getChildImageFile(int i)和getChildTextFile(int i)来获取成员,代码省略
public void killVirus() {
System.out.println("****对文件夹'" + name + "'进行杀毒"); //模拟杀毒
//如果是Folder类型的成员,递归调用Folder的killVirus()方法
for(Object obj : folderList) {
((Folder)obj).killVirus();
}
//如果是ImageFile类型的成员,调用ImageFile的killVirus()方法
for(Object obj : imageList) {
((ImageFile)obj).killVirus();
}
//如果是TextFile类型的成员,调用TextFile的killVirus()方法
for(Object obj : textList) {
((TextFile)obj).killVirus();
}
}
}
编写如下客户端测试代码进行测试:
class Client {
public static void main(String args[]) {
Folder folder1,folder2,folder3;
folder1 = new Folder("Sunny的资料");
folder2 = new Folder("图像文件");
folder3 = new Folder("文本文件");
ImageFile image1,image2;
image1 = new ImageFile("小龙女.jpg");
image2 = new ImageFile("张无忌.gif");
TextFile text1,text2;
text1 = new TextFile("九阴真经.txt");
text2 = new TextFile("葵花宝典.doc");
folder2.addImageFile(image1);
folder2.addImageFile(image2);
folder3.addTextFile(text1);
folder3.addTextFile(text2);
folder1.addFolder(folder2);
folder1.addFolder(folder3);
folder1.killVirus();
}
}
编译并运行程序,输出结果如下:
****对文件夹'Sunny的资料'进行杀毒
****对文件夹'图像文件'进行杀毒
----对图像文件'小龙女.jpg'进行杀毒
----对图像文件'张无忌.gif'进行杀毒
****对文件夹'文本文件'进行杀毒
----对文本文件'九阴真经.txt'进行杀毒
----对文本文件'葵花宝典.doc'进行杀毒
Sunny公司开发人员“成功”实现了杀毒软件的框架设计,但通过仔细分析,发现该设计方案存在如下问题:
(1) 文件夹类Folder的设计和实现都非常复杂,需要定义多个集合存储不同类型的成员,而且需要针对不同的成员提供增加、删除和获取等管理和访问成员的方法,存在大量的冗余代码,系统维护较为困难;
(2) 由于系统没有提供抽象层,客户端代码必须有区别地对待充当容器的文件夹Folder和充当叶子的ImageFile和TextFile,无法统一对它们进行处理;
(3) 系统的灵活性和可扩展性差,如果需要增加新的类型的叶子和容器都需要对原有代码进行修改,例如如果需要在系统中增加一种新类型的视频文件VideoFile,则必须修改Folder类的源代码,否则无法在文件夹中添加视频文件。
面对以上问题,Sunny软件公司的开发人员该如何来解决?这就需要用到本章将要介绍的组合模式,组合模式为处理树形结构提供了一种较为完美的解决方案,它描述了如何将容器和叶子进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器和叶子。
为了让系统具有更好的灵活性和可扩展性,客户端可以一致地对待文件和文件夹,Sunny公司开发人员使用组合模式来进行杀毒软件的框架设计,其基本结构:

AbstractFile充当抽象构件类,Folder充当容器构件类,ImageFile、TextFile和VideoFile充当叶子构件类。完整代码如下所示:
//抽象文件类:抽象构件
abstract class AbstractFile {
public abstract void add(AbstractFile file);
public abstract void remove(AbstractFile file);
public abstract AbstractFile getChild(int i);
public abstract void killVirus();
}
//文件夹类:容器构件
class Folder extends AbstractFile {
//定义集合fileList,用于存储AbstractFile类型的成员
private ArrayList<AbstractFile> fileList=new ArrayList<AbstractFile>();
private String name;
public Folder(String name) {
this.name = name;
}
public void add(AbstractFile file) {
fileList.add(file);
}
public void remove(AbstractFile file) {
fileList.remove(file);
}
public AbstractFile getChild(int i) {
return (AbstractFile)fileList.get(i);
}
public void killVirus() {
System.out.println("****对文件夹'" + name + "'进行杀毒"); //模拟杀毒
//递归调用成员构件的killVirus()方法
for(Object obj : fileList) {
((AbstractFile)obj).killVirus();
}
}
}
//图像文件类:叶子构件
class ImageFile extends AbstractFile {
private String name;
public ImageFile(String name) {
this.name = name;
}
public void add(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public void remove(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public AbstractFile getChild(int i) {
System.out.println("对不起,不支持该方法!");
return null;
}
public void killVirus() {
//模拟杀毒
System.out.println("----对图像文件'" + name + "'进行杀毒");
}
}
//文本文件类:叶子构件
class TextFile extends AbstractFile {
private String name;
public TextFile(String name) {
this.name = name;
}
public void add(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public void remove(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public AbstractFile getChild(int i) {
System.out.println("对不起,不支持该方法!");
return null;
}
public void killVirus() {
//模拟杀毒
System.out.println("----对文本文件'" + name + "'进行杀毒");
}
}
//视频文件类:叶子构件
class VideoFile extends AbstractFile {
private String name;
public VideoFile(String name) {
this.name = name;
}
public void add(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public void remove(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public AbstractFile getChild(int i) {
System.out.println("对不起,不支持该方法!");
return null;
}
public void killVirus() {
//模拟杀毒
System.out.println("----对视频文件'" + name + "'进行杀毒");
}
}
class Client {
public static void main(String args[]) {
//针对抽象构件编程
AbstractFile file1,file2,file3,file4,file5,folder1,folder2,folder3,folder4;
folder1 = new Folder("Sunny的资料");
folder2 = new Folder("图像文件");
folder3 = new Folder("文本文件");
folder4 = new Folder("视频文件");
file1 = new ImageFile("小龙女.jpg");
file2 = new ImageFile("张无忌.gif");
file3 = new TextFile("九阴真经.txt");
file4 = new TextFile("葵花宝典.doc");
file5 = new VideoFile("笑傲江湖.rmvb");
folder2.add(file1);
folder2.add(file2);
folder3.add(file3);
folder3.add(file4);
folder4.add(file5);
folder1.add(folder2);
folder1.add(folder3);
folder1.add(folder4);
//从“Sunny的资料”节点开始进行杀毒操作
folder1.killVirus();
}
}
编译并运行程序,输出结果如下:
****对文件夹'Sunny的资料'进行杀毒
****对文件夹'图像文件'进行杀毒
----对图像文件'小龙女.jpg'进行杀毒
----对图像文件'张无忌.gif'进行杀毒
****对文件夹'文本文件'进行杀毒
----对文本文件'九阴真经.txt'进行杀毒
----对文本文件'葵花宝典.doc'进行杀毒
****对文件夹'视频文件'进行杀毒
----对视频文件'笑傲江湖.rmvb'进行杀毒
由于在本实例中使用了组合模式,在抽象构件类中声明了所有方法,包括用于管理和访问子构件的方法,如add()方法和remove()方法等,因此在ImageFile等叶子构件类中实现这些方法时必须进行相应的异常处理或错误提示。在容器构件类Folder的killVirus()方法中将递归调用其成员对象的killVirus()方法,从而实现对整个树形结构的遍历。
如果需要更换操作节点,例如只需对文件夹“文本文件”进行杀毒,客户端代码只需修改一行即可,将代码:
folder1.killVirus();
改为:
folder3.killVirus();
输出结果如下:
****对文件夹'文本文件'进行杀毒
----对文本文件'九阴真经.txt'进行杀毒
----对文本文件'葵花宝典.doc'进行杀毒
在具体实现时,我们可以创建图形化界面让用户选择所需操作的根节点,无须修改源代码,符合“开闭原则”,客户端无须关心节点的层次结构,可以对所选节点进行统一处理,提高系统的灵活性。
通过引入组合模式,Sunny公司设计的杀毒软件具有良好的可扩展性,在增加新的文件类型时,无须修改现有类库代码,只需增加一个新的文件类作为AbstractFile类的子类即可,但是由于在AbstractFile中声明了大量用于管理和访问成员构件的方法,例如add()、remove()等方法,我们不得不在新增的文件类中实现这些方法,提供对应的错误提示和异常处理。为了简化代码,我们有以下两个解决方案:
将叶子构件的add()、remove()等方法的实现代码移至AbstractFile类中,由AbstractFile提供统一的默认实现,代码如下所示:
//提供默认实现的抽象构件类
abstract class AbstractFile {
public void add(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public void remove(AbstractFile file) {
System.out.println("对不起,不支持该方法!");
}
public AbstractFile getChild(int i) {
System.out.println("对不起,不支持该方法!");
return null;
}
public abstract void killVirus();
}
如果客户端代码针对抽象类AbstractFile编程,在调用文件对象的这些方法时将出现错误提示。如果不希望出现任何错误提示,我们可以在客户端定义文件对象时不使用抽象层,而直接使用具体叶子构件本身,客户端代码片段如下所示:
class Client {
public static void main(String args[]) {
//不能透明处理叶子构件
ImageFile file1,file2;
TextFile file3,file4;
VideoFile file5;
AbstractFile folder1,folder2,folder3,folder4;
//其他代码省略
}
}
这样就产生了一种不透明的使用方式,即在客户端不能全部针对抽象构件类编程,需要使用具体叶子构件类型来定义叶子对象。
除此之外,还有一种解决方法是在抽象构件AbstractFile中不声明任何用于访问和管理成员构件的方法,代码如下所示:
abstract class AbstractFile {
public abstract void killVirus();
}
此时,由于在AbstractFile中没有声明add()、remove()等访问和管理成员的方法,其叶子构件子类无须提供实现;而且无论客户端如何定义叶子构件对象都无法调用到这些方法,不需要做任何错误和异常处理,容器构件再根据需要增加访问和管理成员的方法,但这时候也存在一个问题:客户端不得不使用容器类本身来声明容器构件对象,否则无法访问其中新增的add()、remove()等方法,如果客户端一致性地对待叶子和容器,将会导致容器构件的新增对客户端不可见,客户端代码对于容器构件无法再使用抽象构件来定义,客户端代码片段如下所示:
class Client {
public static void main(String args[]) {
AbstractFile file1,file2,file3,file4,file5;
Folder folder1,folder2,folder3,folder4; //不能透明处理容器构件
//其他代码省略
}
}
在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式:
透明组合模式中,抽象构件Component中声明了所有用于管理成员对象的方法,包括add()、remove()以及getChild()等方法,这样做的好处是确保所有的构件类都有相同的接口。
在客户端看来,叶子对象与容器对象所提供的方法是一致的,客户端可以相同地对待所有的对象。
透明组合模式也是组合模式的标准形式,虽然上面的解决方案一在客户端可以有不透明的实现方法,但是由于在抽象构件中包含add()、remove()等方法,因此它还是透明组合模式,透明组合模式的完整结构:

透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的。
叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供add()、remove()以及getChild()等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)。
安全组合模式中,在抽象构件Component中没有声明任何用于管理成员对象的方法,而是在Composite类中声明并实现这些方法。
这种做法是安全的,因为根本不向叶子对象提供这些管理成员对象的方法,对于叶子对象,客户端不可能调用到这些方法,这就是解决方案二所采用的实现方式。
安全组合模式的结构:

安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
在实际应用中,安全组合模式的使用频率也非常高,在Java AWT中使用的组合模式就是安全组合模式。
在学习和使用组合模式时,Sunny软件公司开发人员发现树形结构其实随处可见,例如Sunny公司的组织结构就是“一棵标准的树”,如图:

在Sunny软件公司的内部办公系统Sunny OA系统中,有一个与公司组织结构对应的树形菜单,行政人员可以给各级单位下发通知,这些单位可以是总公司的一个部门,也可以是一个分公司,还可以是分公司的一个部门。
用户只需要选择一个根节点即可实现通知的下发操作,而无须关心具体的实现细节。这不正是组合模式的“特长”吗?于是Sunny公司开发人员绘制了结构图:

在图11-9中,“单位”充当了抽象构件角色,“公司”充当了容器构件角色,“研发部”、“财务部”和“人力资源部”充当了叶子构件角色。
组合模式使用面向对象的思想来实现树形结构的构建与处理,描述了如何将容器对象和叶子对象进行递归组合,实现简单,灵活性好。
由于在软件开发中存在大量的树形结构,因此组合模式是一种使用频率较高的结构型设计模式,Java SE中的AWT和Swing包的设计就基于组合模式,在这些界面包中为用户提供了大量的容器构件(如Container)和成员构件(如Checkbox、Button和TextComponent等),AWT组合模式结构示意图:

Component类是抽象构件,Checkbox、Button和TextComponent是叶子构件,而Container是容器构件,在AWT中包含的叶子构件还有很多,因为篇幅限制没有在图中一一列出。在一个容器构件中可以包含叶子构件,也可以继续包含容器构件,这些叶子构件和容器构件一起组成了复杂的GUI界面。
除此以外,在XML解析、组织结构树处理、文件系统设计等领域,组合模式都得到了广泛应用。
组合模式的主要优点如下:
(1) 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
(2) 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
(3) 在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
(4) 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
组合模式的主要缺点如下:
在增加新构件时很难对容器中的构件类型进行限制。有时候我们希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件,使用组合模式时,不能依赖类型系统来施加这些约束,因为它们都来自于相同的抽象层,在这种情况下,必须通过在运行时进行类型检查来实现,这个实现过程较为复杂。
在以下情况下可以考虑使用组合模式:
(1) 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。
(2) 在一个使用面向对象语言开发的系统中需要处理一个树形结构。
(3) 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。
装饰模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为,在现实生活中,这种情况也到处存在。
例如一张照片,我们可以不改变照片本身,给它增加一个相框,使得它具有防潮的功能,而且用户可以根据需要给它增加不同类型的相框,甚至可以在一个小相框的外面再套一个大相框。
装饰模式是一种用于替代继承的技术,它通过一种无须定义子类的方式来给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系。在装饰模式中引入了装饰类,在装饰类中既可以调用待装饰的原有类的方法,还可以增加新的方法,以扩充原有类的功能。
装饰模式(Decorator Pattern):动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。
在装饰模式中,为了让系统具有更好的灵活性和可扩展性,我们通常会定义一个抽象装饰类,而将具体的装饰类作为它的子类。

● Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
● ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。
● Decorator(抽象装饰类):它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。
● ConcreteDecorator(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。
由于具体构件类和装饰类都实现了相同的抽象构件接口,因此装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。
装饰模式的核心在于抽象装饰类的设计。
class Decorator implements Component {
private Component component; //维持一个对抽象构件对象的引用
public Decorator(Component component) { //注入一个抽象构件类型的对象
this.component=component;
}
public void operation() {
component.operation(); //调用原有业务方法
}
}
在抽象装饰类Decorator中定义了一个Component类型的对象component,维持一个对抽象构件对象的引用,并可以通过构造方法或Setter方法将一个Component类型的对象注入进来,同时由于Decorator类实现了抽象构件Component接口,因此需要实现在其中声明的业务方法operation(),需要注意的是在Decorator中并未真正实现operation()方法,而只是调用原有component对象的operation()方法,它没有真正实施装饰,而是提供一个统一的接口,将具体装饰过程交给子类完成。
在Decorator的子类即具体装饰类中将继承operation()方法并根据需要进行扩展,典型的具体装饰类代码如下:
class ConcreteDecorator extends Decorator{
public ConcreteDecorator(Component component) {
super(component);
}
public void operation() {
super.operation(); //调用原有业务方法
addedBehavior(); //调用新增业务方法
}
//新增业务方法
public void addedBehavior() {
……
}
}
在具体装饰类中可以调用到抽象装饰类的operation()方法,同时可以定义新的业务方法,如addedBehavior()。
由于在抽象装饰类Decorator中注入的是Component类型的对象,因此我们可以将一个具体构件对象注入其中,再通过具体装饰类来进行装饰;此外,我们还可以将一个已经装饰过的Decorator子类的对象再注入其中进行多次装饰,从而对原有功能的多次扩展。
Sunny软件公司基于面向对象技术开发了一套图形界面构件库VisualComponent,该构件库提供了大量基本构件,如窗体、文本框、列表框等,由于在使用该构件库时,用户经常要求定制一些特效显示效果,如带滚动条的窗体、带黑色边框的文本框、既带滚动条又带黑色边框的列表框等等,因此经常需要对该构件库进行扩展以增强其功能,如图所示:

为了让系统具有更好的灵活性和可扩展性,克服继承复用所带来的问题,Sunny公司开发人员使用装饰模式来重构图形界面构件库的设计,其中部分类的基本结构如图:

Component充当抽象构件类,其子类Window、TextBox、ListBox充当具体构件类,Component类的另一个子类ComponentDecorator充当抽象装饰类,ComponentDecorator的子类ScrollBarDecorator和BlackBorderDecorator充当具体装饰类。完整代码如下所示:
//抽象界面构件类:抽象构件类,为了突出与模式相关的核心代码,对原有控件代码进行了大量的简化
abstract class Component {
public abstract void display();
}
//窗体类:具体构件类
class Window extends Component {
public void display() {
System.out.println("显示窗体!");
}
}
//文本框类:具体构件类
class TextBox extends Component {
public void display() {
System.out.println("显示文本框!");
}
}
//列表框类:具体构件类
class ListBox extends Component {
public void display() {
System.out.println("显示列表框!");
}
}
//构件装饰类:抽象装饰类
class ComponentDecorator extends Component {
//维持对抽象构件类型对象的引用
private Component component;
//注入抽象构件类型的对象
public ComponentDecorator(Component component) {
this.component = component;
}
public void display() {
component.display();
}
}
//滚动条装饰类:具体装饰类
class ScrollBarDecorator extends ComponentDecorator {
public ScrollBarDecorator(Component component) {
super(component);
}
public void display() {
this.setScrollBar();
super.display();
}
public void setScrollBar() {
System.out.println("为构件增加滚动条!");
}
}
//黑色边框装饰类:具体装饰类
class BlackBorderDecorator extends ComponentDecorator {
public BlackBorderDecorator(Component component) {
super(component);
}
public void display() {
this.setBlackBorder();
super.display();
}
public void setBlackBorder() {
System.out.println("为构件增加黑色边框!");
}
}
编写如下客户端测试代码:
class Client {
public static void main(String args[]) {
//1. 先定义了一个Window类型的具体构件对象component
Component component = new Window(); //定义具体构件
//2. 将component作为构造函数的参数注入到具体装饰类ScrollBarDecorator中
Component componentSB = new ScrollBarDecorator(component); //定义装饰后的构件
componentSB.display();
System.out.println("--------------------------------------------------");
//希望得到一个既有滚动条又有黑色边框的窗体
//3. 将装饰了一次之后的对象componentSB继续注入到另一个装饰类中,进行第二次装饰
Component componentBB= new BlackBorderDecorator(componentSB);
componentBB.display();
}
}
编译并运行程序,输出结果如下:
为构件增加滚动条!
显示窗体!
--------------------------------------------------
为构件增加黑色边框!
为构件增加滚动条!
显示窗体!
我们可以将装饰了一次之后的componentSB对象注入另一个装饰类BlackBorderDecorator中实现第二次装饰,得到一个经过两次装饰的对象componentBB,再调用componentBB的display()方法即可得到一个既有滚动条又有黑色边框的窗体。
装饰模式虽好,但存在一个问题。如果客户端希望单独调用具体装饰类新增的方法,而不想通过抽象构件中声明的方法来调用新增方法时将遇到一些麻烦,我们通过一个实例来对这种情况加以说明:
在Sunny软件公司开发的Sunny OA系统中,采购单(PurchaseRequest)和请假条(LeaveRequest)等文件(Document)对象都具有显示功能,现在要为其增加审批、删除等功能,使用装饰模式进行设计。
我们使用装饰模式可以得到如图12-5所示结构图:

文件对象功能增加实例结构图
Document充当抽象构件类,PurchaseRequest和LeaveRequest充当具体构件类,Decorator充当抽象装饰类,Approver和Deleter充当具体装饰类。
示例代码如下所示:
abstract class Document {
public abstract void display();
}
// 采购单
public class PurchaseRequest extends Document{
@Override
public void display() {
System.out.println("采购单");
}
}
public class LeaveRequest extends Document{
@Override
public void display() {
System.out.println("请假条");
}
}
public class Decorator extends Document{
private final Document document;
public Decorator(Document document){
this.document = document;
}
@Override
public void display() {
document.display();
}
}
// 审批方法直接继承了Decorator类的方法
public class Approver extends Decorator{
public Approver(Document document) {
super(document);
System.out.println("新增审批功能!");
}
public void approve() {
System.out.println("审批中...");
}
}
public class Deleter extends Decorator{
public Deleter(Document document) {
super(document);
System.out.println("新增删除功能!");
}
}
大家注意,Approver类继承了抽象装饰类Decorator的display()方法,同时新增了业务方法approve(),但这两个方法是独立的,没有任何调用关系。如果客户端需要分别调用这两个方法,代码片段如下所示:
public class Client {
public static void main(String[] args) {
Document doc;
doc = new PurchaseRequest();
Approver newDoc;
newDoc = new Approver(doc);
newDoc.display(); //调用原有业务方法
newDoc.approve(); // 调用新增业务方法
}
}
如果newDoc也使用Document类型来定义,将导致客户端无法调用新增业务方法approve(),因为在抽象构件类Document中没有对approve()方法的声明。也就是说,在客户端无法统一对待装饰之前的具体构件对象和装饰之后的构件对象。
在实际使用过程中,由于新增行为可能需要单独调用,因此这种形式的装饰模式也经常出现,这种装饰模式被称为半透明****(Semi-transparent)装饰模式,而标准的装饰模式是透明****(Transparent)装饰模式。
总结透明装饰模式和半透明装饰模式
在透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该将对象声明为具体构件类型或具体装饰类型,而应该全部声明为抽象构件类型。
对于客户端而言,具体构件对象和具体装饰对象没有任何区别。
// 正确的用法
Component c, c1; //使用抽象构件类型定义对象
c = new ConcreteComponent();
c1 = new ConcreteDecorator (c);
// 错误的用法
ConcreteComponent c; //使用具体构件类型定义对象
c = new ConcreteComponent();
ConcreteDecorator c1; //使用具体装饰类型定义对象
c1 = new ConcreteDecorator(c)
透明装饰模式可以让客户端透明地使用装饰之前的对象和装饰之后的对象,无须关心它们的区别,此外,还可以对一个已装饰过的对象进行多次装饰,得到更为复杂、功能更为强大的对象。
在实现透明装饰模式时,要求具体装饰类的operation()方法覆盖抽象装饰类的operation()方法,除了调用原有对象的operation()外还需要调用新增的addedBehavior()方法来增加新行为。
透明装饰模式的设计难度较大,而且有时我们需要单独调用新增的业务方法。
为了能够调用到新增方法,我们不得不用具体装饰类型来定义装饰之后的对象,而具体构件类型还是可以使用抽象构件类型来定义,这种装饰模式即为半透明装饰模式,
也就是说,对于客户端而言,具体构件类型无须关心,是透明的;但是具体装饰类型必须指定,这是不透明的。
如本节前面所提到的文件对象功能增加实例,为了能够调用到在Approver中新增方法approve(),客户端代码
Document doc; //使用抽象构件类型定义
doc = new PurchaseRequest();
Approver newDoc; //使用具体装饰类型定义
newDoc = new Approver(doc);
半透明装饰模式可以给系统带来更多的灵活性,设计相对简单,使用起来也非常方便;但是其最大的缺点在于不能实现对同一个对象的多次装饰,而且客户端需要有区别地对待装饰之前的对象和装饰之后的对象。
在实现半透明的装饰模式时,我们只需在具体装饰类中增加一个独立的addedBehavior()方法来封装相应的业务处理,由于客户端使用具体装饰类型来定义装饰后的对象,因此可以单独调用addedBehavior()方法来扩展系统功能。
在使用装饰模式时,通常我们需要注意以下几个问题:
(1) 尽量保持装饰类的接口与被装饰类的接口相同,这样,对于客户端而言,无论是装饰之前的对象还是装饰之后的对象都可以一致对待。这也就是说,在可能的情况下,我们应该尽量使用透明装饰模式。
(2) 尽量保持具体构件类ConcreteComponent是一个“轻”类,也就是说不要把太多的行为放在具体构件类中,我们可以通过装饰类对其进行扩展。
(3) 如果只有一个具体构件类,那么抽象装饰类可以作为该具体构件类的直接子类。如图12-6所示:

装饰模式降低了系统的耦合度,可以动态增加或删除对象的职责,并使得需要装饰的具体构件类和具体装饰类可以独立变化,以便增加新的具体构件类和具体装饰类。
在软件开发中,装饰模式应用较为广泛,例如在JavaIO中的输入流和输出流的设计、javax.swing包中一些图形界面构件功能的增强等地方都运用了装饰模式。
装饰模式的主要优点如下:
(1) 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
(2) 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为。
(3) 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
(4) 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合“开闭原则”。
装饰模式的主要缺点如下:
(1) 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能。
(2) 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。
在以下情况下可以考虑使用装饰模式:
(1) 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
(2) 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。
不能采用继承的情况主要有两类:
第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;
第二类是因为类已定义为不能被继承(如Java语言中的final类)。
外观模式中,一个子系统的外部与其内部的通信通过一个统一的外观类进行,外观类将客户类与子系统的内部复杂性分隔开,使得客户类只需要与外观角色打交道,而不需要与子系统内部的很多对象打交道。
外观模式:为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
外观模式又称为门面模式,它是一种对象结构型模式。外观模式是迪米特法则的一种具体实现,通过引入一个新的外观角色可以降低原有系统的复杂度,同时降低客户类与子系统的耦合度。
外观模式没有一个一般化的类图描述,通常使用下图表示:

(1) Facade(外观角色):在客户端可以调用它的方法,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求委派到相应的子系统去,传递给相应的子系统对象处理。
(2) SubSystem(子系统角色):在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能;每一个子系统都可以被客户端直接调用,或者被外观角色调用,它处理由外观类传过来的请求;子系统并不知道外观的存在,对于子系统而言,外观角色仅仅是另外一个客户端而已。
模式实现 外观模式的主要目的在于降低系统的复杂程度,在面向对象软件系统中,类与类之间的关系越多,不能表示系统设计得越好,反而表示系统中类之间的耦合度太大,这样的系统在维护和修改时都缺乏灵活性,因为一个类的改动会导致多个类发生变化,而外观模式的引入在很大程度上降低了类与类之间的耦合关系。引入外观模式之后,增加新的子系统或者移除子系统都非常方便,客户类无须进行修改(或者极少的修改),只需要在外观类中增加或移除对子系统的引用即可。从这一点来说,外观模式在一定程度上并不符合开闭原则,增加新的子系统需要对原有系统进行一定的修改,虽然这个修改工作量不大。
外观模式中所指的子系统是一个广义的概念,它可以是一个类、一个功能模块、系统的一个组成部分或者一个完整的系统。子系统类通常是一些业务类,实现了一些具体的、独立的业务功能,其典型代码如下:
class SubSystemA
{
public void MethodA()
{
//业务实现代码
}
}
class SubSystemB
{
public void MethodB()
{
//业务实现代码
}
}
class SubSystemC
{
public void MethodC()
{
//业务实现代码
}
}
在引入外观类之后,与子系统业务类之间的交互统一由外观类来完成,在外观类中通常存在如下代码:
class Facade
{
private SubSystemA obj1 = new SubSystemA();
private SubSystemB obj2 = new SubSystemB();
private SubSystemC obj3 = new SubSystemC();
public void Method()
{
obj1.MethodA();
obj2.MethodB();
obj3.MethodC();
}
}
由于在外观类中维持了对子系统对象的引用,客户端可以通过外观类来间接调用子系统对象的业务方法,而无须与子系统对象直接交互。引入外观类后,客户端代码变得非常简单,典型代码如下:
class Program
{
static void Main(string[] args)
{
Facade facade = new Facade();
facade.Method();
}
}
某软件公司欲开发一个可应用于多个软件的文件加密模块,该模块可以对文件中的数据进行加密并将加密之后的数据存储在一个新文件中,具体的流程包括三个部分,分别是读取源文件、加密、保存加密之后的文件,其中,读取文件和保存文件使用流来实现,加密操作通过求模运算实现。这三个操作相对独立,为了实现代码的独立重用,让设计更符合单一职责原则,这三个操作的业务代码封装在三个不同的类中。
现使用外观模式设计该文件加密模块。
通过分析,本实例结构图如图

EncryptFacade充当外观类,FileReader、CipherMachine和FileWriter充当子系统类。
// FileReader:文件读取类,充当子系统类。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
public class FileReader {
public String read(String fileNameSrc) throws IOException {
StringBuilder data = new StringBuilder();
Path p = Paths.get(fileNameSrc);
Stream<String> lines = Files.lines(p);
lines.forEach(line -> data.append(line).append("\n"));
lines.close();
System.out.println("读取文件,获取明文:" + data.toString());
return data.toString();
}
}
//CipherMachine:数据加密类,充当子系统类
public class CipherMachine {
public String encrypt(String text) {
StringBuilder es = new StringBuilder();
char[] chars = text.toCharArray();
for(char ch : chars) {
int c = ch % 7;
String s = String.valueOf(c);
es.append(s);
}
System.out.println("数据加密,将明文转换为密文:" + es);
return es.toString();
}
}
FileWriter:文件保存类,充当子系统类
import java.io.IOException;
public class FileWriter {
public void write(String encryptStr, String fileNameDes) throws IOException {
System.out.println("保存密文,写入文件。");
java.io.FileWriter fileWriter = new java.io.FileWriter(fileNameDes);
fileWriter.write(encryptStr);
fileWriter.flush();
}
}
public class EncryptFacade {
private FileReader reader;
private FileWriter writer;
private CipherMachine cipherMachine;
public EncryptFacade() {
this.reader = new FileReader();
this.writer = new FileWriter();
this.cipherMachine = new CipherMachine();
}
public void fileEncrypt(String fileSrc, String fileDes) throws IOException {
String text = reader.read(fileSrc);
String es = cipherMachine.encrypt(text);
writer.write(es, fileDes);
}
}
客户端代码
import java.io.IOException;
public class Client {
public static void main(String[] args) throws IOException {
EncryptFacade ef = new EncryptFacade();
ef.fileEncrypt("D:\\src.txt", "D:\\des.txt");
}
}
D:\src.txt 内容
Hello World!
运行结果
读取文件,获取明文:Hello World!
数据加密,将明文转换为密文:2333643623263
保存密文,写入文件。
在标准的外观模式结构图中,如果需要增加、删除或更换与外观类交互的子系统类,必须修改外观类或客户端的源代码,这将违背开闭原则,因此可以通过引入抽象外观类来对系统进行改进,在一定程度上可以解决该问题。在引入抽象外观类之后,客户端可以针对抽象外观类进行编程,对于新的业务需求,不需要修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象
如果在应用实例“文件加密模块”中需要更换一个加密类,不再使用原有的基于求模运算的加密类CipherMachine,而改为基于移位运算的新加密类NewCipherMachine
public class NewCipherMachine {
public String Encrypt(String plainText) {
StringBuilder es = new StringBuilder();
int key = 10;
char[] chars = plainText.toCharArray();
for(char c : chars) {
int temp = c;
if (c >= 'a' && c <= 'z') {
temp += key % 26;
if (temp > 122) {
temp -= 26;
}
if (temp < 97) {
temp += 26;
}
}
if (c >= 'A' && c <= 'Z') {
temp += key % 26;
if (temp > 90) {
temp -= 26;
}
if (temp < 64) {
temp += 26;
}
}
String s = String.valueOf(temp);
es.append(s);
}
return es.toString();
}
}
如果不增加新的外观类,只能通过修改原有外观类EncryptFacade的源代码来实现加密类的更换,将原有的对CipherMachine类型对象的引用改为对NewCipherMachine类型对象的引用,这违背了开闭原则,因此需要通过增加新的外观类来实现对子系统对象引用的改变。
如果增加一个新的外观类NewEncryptFacade来与FileReader类、FileWriter类以及新增加的NewCipherMachine类进行交互,虽然原有系统类库无须做任何修改,但是因为客户端代码中原来针对EncryptFacade类进行编程,现在需要改为NewEncryptFacade类,因此需要修改客户端源代码。
如何在不修改客户端代码的前提下使用新的外观类呢?解决方法之一是:引入一个抽象外观类,客户端针对抽象外观类编程,而在运行时再确定具体外观类,引入抽象外观类之后的文件加密模块结构图如图
客户类Client针对抽象外观类AbstractEncryptFacade进行编程,AbstractEncryptFacade代码如下
import java.io.IOException;
public abstract class AbstractEncryptFacade {
abstract void fileEncrypt(String fileNameSrc, String fileNameDes) throws IOException;
}
新增具体加密外观类NewEncryptFacade
import java.io.IOException;
public class NewEncryptFacade extends AbstractEncryptFacade{
private final FileReader reader;
private final FileWriter writer;
private final NewCipherMachine cipherMachine;
public NewEncryptFacade() {
this.reader = new FileReader();
this.writer = new FileWriter();
this.cipherMachine = new NewCipherMachine();
}
@Override
public void fileEncrypt(String fileNameSrc, String fileNameDes) throws IOException {
String text = reader.read(fileNameSrc);
String es = cipherMachine.Encrypt(text);
writer.write(es, fileNameDes);
}
}
原 EncryptFacade 也实现 AbstractEncryptFacade
import org.example.demo01.FileReader;
import org.example.demo01.FileWriter;
import java.io.IOException;
public class EncryptFacade extends AbstractEncryptFacade {
private org.example.demo01.FileReader reader;
private org.example.demo01.FileWriter writer;
private CipherMachine cipherMachine;
public EncryptFacade() {
this.reader = new FileReader();
this.writer = new FileWriter();
this.cipherMachine = new CipherMachine();
}
public void fileEncrypt(String fileSrc, String fileDes) throws IOException {
String text = reader.read(fileSrc);
String es = cipherMachine.encrypt(text);
writer.write(es, fileDes);
}
}
客户端
import java.io.IOException;
public class Client {
public static void main(String[] args) throws IOException {
//客户端调用不同的 封装门面类
AbstractEncryptFacade af = new NewEncryptFacade();
af.fileEncrypt("D:\\src.txt", "D:\\des2.txt");
AbstractEncryptFacade af2 = new EncryptFacade();
af2.fileEncrypt("D:\\src.txt", "D:\\des.txt");
}
}
外观模式是一种使用频率非常高的设计模式,它通过引入一个外观角色来简化客户端与子系统之间的交互,为复杂的子系统调用提供一个统一的入口,使子系统与客户端的耦合度降低,且客户端调用非常方便。外观模式并不给系统增加任何新功能,它仅仅是简化调用接口。在几乎所有的软件中都能够找到外观模式的应用,如绝大多数B/S系统都有一个首页或者导航页面,大部分C/S系统都提供了菜单或者工具栏,在这里,首页和导航页面就是B/S系统的外观角色,而菜单和工具栏就是C/S系统的外观角色,通过它们用户可以快速访问子系统,降低了系统的复杂程度。所有涉及到与多个业务对象交互的场景都可以考虑使用外观模式进行重构。
它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易。通过引入外观模式,客户端代码将变得很简单,与之关联的对象也很少。
它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可。
一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。
不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活 性。
如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则。
当要为访问一系列复杂的子系统提供一个简单入口时可以使用外观模式。
客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。
在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
当一个软件系统在运行时产生的对象数量太多,将导致运行代价过高,带来系统性能下降等问题。
例如在一个文本字符串中存在很多重复的字符,如果每一个字符都用一个单独的对象来表示,将会占用较多的内存空间,那么我们如何去避免系统中出现大量相同或相似的对象,同时又不影响客户端程序通过面向对象的方式对这些对象进行操作?享元模式正为解决这一类问题而诞生。
享元模式通过共享技术实现相同或相似对象的重用,在逻辑上每一个出现的字符都有一个对象与之对应,然而在物理上它们却共享同一个享元对象,这个对象可以出现在一个字符串的不同地方,相同的字符对象都指向同一个实例,在享元模式中,存储这些共享实例对象的地方称为享元池(Flyweight Pool)。
我们可以针对每一个不同的字符创建一个享元对象,将其放在享元池中,需要时再从享元池取出。如图

享元模式以共享的方式高效地支持大量细粒度对象的重用,享元对象能做到共享的关键是区分了内部状态(Intrinsic State)和外部状态(Extrinsic State)。
下面将对享元的内部状态和外部状态进行简单的介绍:
内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享。如字符的内容,不会随外部环境的变化而变化,无论在任何环境下字符“a”始终是“a”,都不会变成“b”。
外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态通常由客户端保存,并在享元对象被创建之后,需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。如字符的颜色,可以在不同的地方有不同的颜色,例如有的“a”是红色的,有的“a”是绿色的,字符的大小也是如此,有的“a”是五号字,有的“a”是四号字。而且字符的颜色和大小是两个独立的外部状态,它们可以独立变化,相互之间没有影响,客户端可以在使用时将外部状态注入享元对象中。
正因为区分了内部状态和外部状态,我们可以将具有相同内部状态的对象存储在享元池中,享元池中的对象是可以实现共享的,需要的时候就将对象从享元池中取出,实现对象的复用。通过向取出的对象注入不同的外部状态,可以得到一系列相似的对象,而这些对象在内存中实际上只存储一份。
享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。
享元模式结构较为复杂,一般结合工厂模式一起使用,在它的结构图中包含了一个享元工厂类

● Flyweight(抽象享元类):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
● ConcreteFlyweight(具体享元类):它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
● UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
● FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。
在享元模式中引入了享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,当用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。
class FlyweightFactory {
//定义一个HashMap用于存储享元对象,实现享元池
private HashMap flyweights = newHashMap();
public Flyweight getFlyweight(String key){
//如果对象存在,则直接从享元池获取
if(flyweights.containsKey(key)){
return(Flyweight)flyweights.get(key);
}
//如果对象不存在,先创建一个新的对象添加到享元池中,然后返回
else {
Flyweight fw = newConcreteFlyweight();
flyweights.put(key,fw);
return fw;
}
}
}
享元类的设计是享元模式的要点之一,在享元类中要将内部状态和外部状态分开处理,通常将内部状态作为享元类的成员变量,而外部状态通过注入的方式添加到享元类中。典型的享元类代码如下所示
class Flyweight {
//内部状态intrinsicState作为成员变量,同一个享元对象其内部状态是一致的
private String intrinsicState;
public Flyweight(String intrinsicState) {
this.intrinsicState=intrinsicState;
}
//外部状态extrinsicState在使用时由外部设置,不保存在享元对象中,即使是同一个对象,在每一次调用时也可以传入不同的外部状态
public void operation(String extrinsicState) {
......
}
}

Sunny软件公司开发人员通过对围棋软件进行分析,发现在围棋棋盘中包含大量的黑子和白子,它们的形状、大小都一模一样,只是出现的位置不同而已。如果将每一个棋子都作为一个独立的对象存储在内存中,将导致该围棋软件在运行时所需内存空间较大,如何降低运行代价、提高系统性能是Sunny公司开发人员需要解决的一个问题。
为了节约存储空间,提高系统性能,Sunny公司开发人员使用享元模式来设计围棋软件中的棋子,其基本结构

IgoChessman充当抽象享元类,BlackIgoChessman和WhiteIgoChessman充当具体享元类,IgoChessmanFactory充当享元工厂类。完整代码
abstract class IgoChessman {
public abstract String getColor();
public void display() {
System.out.println("棋子颜色:" + this.getColor());
}
}
//黑色棋子类:具体享元类
class BlackIgoChessman extends IgoChessman {
public String getColor() {
return "黑色";
}
}
//白色棋子类:具体享元类
class WhiteIgoChessman extends IgoChessman {
public String getColor() {
return "白色";
}
}
import java.util.Hashtable;
//围棋棋子工厂类:享元工厂类,使用单例模式进行设计
class IgoChessmanFactory {
//返回享元工厂类的唯一实例
@Getter
private static IgoChessmanFactory instance = new IgoChessmanFactory();
private static Hashtable<String, IgoChessman> ht; //使用Hashtable来存储享元对象,充当享元池
private IgoChessmanFactory() {
ht = new Hashtable<>();
IgoChessman black,white;
black = new BlackIgoChessman();
ht.put("b",black);
white = new WhiteIgoChessman();
ht.put("w",white);
}
//通过key来获取存储在Hashtable中的享元对象
public static IgoChessman getIgoChessman(String color) {
return ht.get(color);
}
}
class Client {
public static void main(String[] args) {
IgoChessman black1,black2,black3,white1,white2;
IgoChessmanFactory factory;
//获取享元工厂对象
factory = IgoChessmanFactory.getInstance();
//通过享元工厂获取三颗黑子
black1 = IgoChessmanFactory.getIgoChessman("b");
black2 = IgoChessmanFactory.getIgoChessman("b");
black3 = IgoChessmanFactory.getIgoChessman("b");
System.out.println("判断两颗黑子是否相同:" + (black1==black2));
//通过享元工厂获取两颗白子
white1 = IgoChessmanFactory.getIgoChessman("w");
white2 = IgoChessmanFactory.getIgoChessman("w");
System.out.println("判断两颗白子是否相同:" + (white1==white2));
//显示棋子
black1.display();
black2.display();
black3.display();
white1.display();
white2.display();
}
}
运行结果如下
判断两颗黑子是否相同:true
判断两颗白子是否相同:true
棋子颜色:黑色
棋子颜色:黑色
棋子颜色:黑色
棋子颜色:白色
棋子颜色:白色
从输出结果可以看出,虽然我们获取了三个黑子对象和两个白子对象,但是它们的内存地址相同,也就是说,它们实际上是同一个对象。
在实现享元工厂类时我们使用了单例模式和简单工厂模式,确保了享元工厂对象的唯一性,并提供工厂方法来向客户端返回享元对象。
虽然黑色棋子和白色棋子可以共享,但是它们将显示在棋盘的不同位置,如何让相同的黑子或者白子能够多次重复显示且位于一个棋盘的不同地方?
解决方法就是将棋子的位置定义为棋子的一个外部状态,在需要时再进行设置。因此,我们增加了一个新的类Coordinates(坐标类),用于存储每一个棋子的位置,修改之后的结构图
除了增加一个坐标类Coordinates以外,抽象享元类IgoChessman中的display()方法也将对应增加一个Coordinates类型的参数,用于在显示棋子时指定其坐标,Coordinates类和修改之后的IgoChessman类的代码如下所示
//坐标类:外部状态类
class Coordinates {
private int x;
private int y;
public Coordinates(int x,int y) {
this.x = x;
this.y = y;
}
public int getX() {
return this.x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return this.y;
}
public void setY(int y) {
this.y = y;
}
}
//围棋棋子类:抽象享元类
abstract class IgoChessman {
public abstract String getColor();
public void display(Coordinates coord){
System.out.println("棋子颜色:" + this.getColor() + ",棋子位置:" + coord.getX() + "," + coord.getY() );
}
}
客户端代码
class Client {
public static void main(String args[]) {
IgoChessman black1,black2,black3,white1,white2;
IgoChessmanFactory factory;
//获取享元工厂对象
factory = IgoChessmanFactory.getInstance();
//通过享元工厂获取三颗黑子
black1 = factory.getIgoChessman("b");
black2 = factory.getIgoChessman("b");
black3 = factory.getIgoChessman("b");
System.out.println("判断两颗黑子是否相同:" + (black1==black2));
//通过享元工厂获取两颗白子
white1 = factory.getIgoChessman("w");
white2 = factory.getIgoChessman("w");
System.out.println("判断两颗白子是否相同:" + (white1==white2));
//显示棋子,同时设置棋子的坐标位置
black1.display(new Coordinates(1,2));
black2.display(new Coordinates(3,4));
black3.display(new Coordinates(1,3));
white1.display(new Coordinates(2,5));
white2.display(new Coordinates(2,4));
}
}
输出:
判断两颗白子是否相同:true
棋子颜色:黑色,棋子位置:1,2
棋子颜色:黑色,棋子位置:3,4
棋子颜色:黑色,棋子位置:1,3
棋子颜色:白色,棋子位置:2,5
棋子颜色:白色,棋子位置:2,4
从输出结果可以看到,在每次调用display()方法时,都设置了不同的外部状态——坐标值,因此相同的棋子对象虽然具有相同的颜色,但是它们的坐标值不同,将显示在棋盘的不同位置。
标准的享元模式结构图中既包含可以共享的具体享元类,也包含不可以共享的非共享具体享元类。但是在实际使用过程中,我们有时候会用到两种特殊的享元模式:单纯享元模式和复合享元模式,下面将对这两种特殊的享元模式进行简单的介绍:
在单纯享元模式中,所有的具体享元类都是可以共享的,不存在非共享具体享元类。单纯享元模式的结构如图:

将一些单纯享元对象使用组合模式加以组合,还可以形成复合享元对象,这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享。复合享元模式的结构如图:

通过复合享元模式,可以确保复合享元类CompositeConcreteFlyweight中所包含的每个单纯享元类ConcreteFlyweight都具有相同的外部状态,而这些单纯享元的内部状态往往可以不同。
如果希望为多个内部状态不同的享元对象设置相同的外部状态,可以考虑使用复合享元模式。
享元模式通常需要和其他模式一起联用,几种常见的联用方式如下:
(1)在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象,使用简单工厂模式来生成享元对象。
(2)在一个系统中,通常只有唯一一个享元工厂,因此可以使用单例模式进行享元工厂类的设计。
(3)享元模式可以结合组合模式形成复合享元模式,统一对多个享元对象设置外部状态。
JDK类库中的String类使用了享元模式,我们通过如下代码来加以说明:
class Demo {
public static void main(String args\[\]) {
String str1 = "abcd";
String str2 = "abcd";
String str3 = "ab" + "cd";
String str4 = "ab";
str4 += "cd";
System.out.println(str1 == str2);
System.out.println(str1 == str3);
System.out.println(str1 == str4);
str2 += "e";
System.out.println(str1 == str2);
}
}
在Java语言中,如果每次执行类似String str1=“abcd"的操作时都创建一个新的字符串对象将导致内存开销很大,因此如果第一次创建了内容为"abcd"的字符串对象str1,下一次再创建内容相同的字符串对象str2时会将它的引用指向"abcd”,不会重新分配内存空间,从而实现了"abcd"在内存中的共享。上述代码输出结果如下:
true
true
false
false
可以看出,前两个输出语句均为true,说明str1、str2、str3在内存中引用了相同的对象;如果有一个字符串str4,其初值为"ab",再对它进行操作str4 += “cd”,此时虽然str4的内容与str1相同,但是由于str4的初始值不同,在创建str4时重新分配了内存,所以第三个输出语句结果为false;最后一个输出语句结果也为false,说明当对str2进行修改时将创建一个新的对象,修改工作在新对象上完成,而原来引用的对象并没有发生任何改变,str1仍然引用原有对象,而str2引用新对象,str1与str2引用了两个完全不同的对象。
扩展
关于Java String类这种在修改享元对象时,先将原有对象复制一份,然后在新对象上再实施修改操作的机制称为“Copy On Write”,大家可以自行查询相关资料来进一步了解和学习“Copy On Write”机制,在此不作详细说明。
当系统中存在大量相同或者相似的对象时,享元模式是一种较好的解决方案,它通过共享技术实现相同或相似的细粒度对象的复用,从而节约了内存空间,提高了系统性能。相比其他结构型设计模式,享元模式的使用频率并不算太高,但是作为一种以“节约内存,提高性能”为出发点的设计模式,它在软件开发中还是得到了一定程度的应用。
享元模式的主要优点如下:
(1) 可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。
(2) 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
享元模式的主要缺点如下:
(1) 享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
(2) 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。
在以下情况下可以考虑使用享元模式:
(1) 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
(2) 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
(3) 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
代理模式是常用的结构型设计模式之一,当无法直接访问某个对象或访问某个对象存在困难时可以通过一个代理对象来间接访问,为了保证客户端使用的透明性,所访问的真实对象与代理对象需要实现相同的接口。
根据代理模式的使用目的不同,代理模式又可以分为多种类型,例如保护代理、远程代理、虚拟代理、缓冲代理等,它们应用于不同的场合,满足用户的不同需求。
近年来,代购已逐步成为电子商务的一个重要分支。
何谓代购,简单来说就是找人帮忙购买所需要的商品,当然你可能需要向实施代购的人支付一定的费用。
代购通常分为两种类型:
一种是因为在当地买不到某件商品,又或者是因为当地这件商品的价格比其他地区的贵,因此托人在其他地区甚至国外购买该商品,然后通过快递发货或者直接携带回来;
还有一种代购,由于消费者对想要购买的商品相关信息的缺乏,自已无法确定其实际价值而又不想被商家宰,只好委托中介机构帮其讲价或为其代买。
代购网站为此应运而生,它为消费者提供在线的代购服务,如果看中某国外购物网站上的商品,可以登录代购网站填写代购单并付款,代购网站会帮助进行购买然后通过快递公司将商品发送给消费者。

在软件开发中,也有一种设计模式可以提供与代购网站类似的功能。由于某些原因,客户端不想或不能直接访问一个对象,此时可以通过一个称之为“代理”的第三者来实现间接访问,该方案对应的设计模式被称为代理模式。
代理模式是一种应用很广泛的结构型设计模式,而且变化形式非常多,常见的代理形式包括远程代理、保护代理、虚拟代理、缓冲代理、智能引用代理等,后面将学习这些不同的代理形式。
代理模式:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。
Proxy Pattern: Provide a surrogate or placeholder for another object to control access to it.
代理模式是一种对象结构型模式。在代理模式中引入了一个新的代理对象,代理对象在客户端对象和目标对象之间起到中介的作用,它去掉客户不能看到的内容和服务或者增添客户需要的额外的新服务。
代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层,代理模式结构如图

**(1) Subject(抽象主题角色):**它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。
**(2) Proxy(代理主题角色):**它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象;在代理主题角色中提供一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题;代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。通常,在代理主题角色中,客户端在调用所引用的真实主题操作之前或之后还需要执行其他操作,而不仅仅是单纯调用真实主题对象中的操作。
**(3) RealSubject(真实主题角色):**它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。
代理模式的结构图比较简单,但是在真实的使用和实现过程中要复杂很多,特别是代理类的设计和实现。
抽象主题类声明了真实主题类和代理类的公共方法,它可以是接口、抽象类或具体类,客户端针对抽象主题类编程,一致性地对待真实主题和代理主题,典型的抽象主题类代码如下:
abstract class Subject
{
public abstract void Request();
}
真实主题类继承了抽象主题类,提供了业务方法的具体实现,其典型代码如下:
class RealSubject extend Subject
{
public override void Request()
{
//业务方法具体实现代码
}
}
代理类也是抽象主题类的子类,它维持一个对真实主题对象的引用,调用在真实主题中实现的业务方法,在调用时可以在原有业务方法的基础上附加一些新的方法来对功能进行扩充或约束,最简单的代理类实现代码如下:
class Proxy extend Subject
{
private RealSubject realSubject = new RealSubject(); //维持一个对真实主题对象的引用
public void PreRequest()
{
…...
}
public override void Request()
{
PreRequest();
realSubject.Request(); //调用真实主题对象的方法
PostRequest();
}
public void PostRequest()
{
……
}
}
在实际开发过程中,代理类的实现比上述代码要复杂很多,代理模式根据其目的和实现方式不同可分为很多种类,其中常用的几种代理模式简要说明如下:
(1) 远程代理(Remote Proxy):为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又称为大使(Ambassador)。
(2) 虚拟代理(Virtual Proxy):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
(3) 保护代理(Protect Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
(4) 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
(5) 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等。
在这些常用的代理模式中,有些代理类的设计非常复杂,例如远程代理类,它封装了底层网络通信和对远程对象的调用,其实现较为复杂。
某软件公司承接了某信息咨询公司的收费商务信息查询系统的开发任务,该系统的基本需求如下:
(1) 在进行商务信息查询之前用户需要通过身份验证,只有合法用户才能够使用该查询系统;
(2) 在进行商务信息查询时系统需要记录查询日志,以便根据查询次数收取查询费用。
该软件公司开发人员已完成了商务信息查询模块的开发任务,现希望能够以一种松耦合的方式向原有系统增加身份验证和日志记录功能,客户端代码可以无区别地对待原始的商务信息查询模块和增加新功能之后的商务信息查询模块,而且可能在将来还要在该信息查询模块中增加一些新的功能。
试使用代理模式设计并实现该收费商务信息查询系统。
通过分析,可以采用一种间接访问的方式来实现该商务信息查询系统的设计,在客户端对象和信息查询对象之间增加一个代理对象,让代理对象来实现身份验证和日志记录等功能,而无须直接对原有的商务信息查询对象进行修改

商务信息查询系统设计方案示意图
客户端对象通过代理对象间接访问具有商务信息查询功能的真实对象,在代理对象中除了调用真实对象的商务信息查询功能外,还增加了身份验证和日志记录等功能。使用代理模式设计该商务信息查询系统,结构图所示。

商务信息查询系统结构图
在图中,业务类AccessValidator用于验证用户身份,业务类Logger用于记录用户查询日志,Searcher充当抽象主题角色,RealSearcher充当真实主题角色,ProxySearcher充当代理主题角色。
/**
* AccessValidator:身份验证类,业务类,它提供方法Validate()来实现身份验证。
*/
public class AccessValidator {
public Boolean validate(String userId) {
System.out.println("在数据库中验证用户'" + userId + "'是否是合法用户");
if (userId.equals("杨过")) {
System.out.println(userId + "登录成功");
return true;
} else {
System.out.println(userId + "登录失败");
return false;
}
}
}
/**
* Logger:日志记录类,业务类,它提供方法Log()来保存日志。
*/
public class Logger {
public void log(String userId) {
System.out.printf("更新数据库,用户'%s'查询次数加1!\n", userId);
}
}
/**
* Searcher:抽象查询类,充当抽象主题角色,它声明了DoSearch()方法。
*/
public interface Searcher {
String doSearch(String userId, String keyword);
}
/**
* RealSearcher:具体查询类,充当真实主题角色,它实现查询功能,提供方法DoSearch()来查询信息。
*/
public class RealSearcher implements Searcher{
// 模拟查询商务信息
@Override
public String doSearch(String userId, String keyword) {
System.out.printf("用户'%s'使用关键词'%s'查询商务信息!\n",userId,keyword);
return "返回具体内容";
}
}
/**
* ProxySearcher:代理查询类,充当代理主题角色,它是查询代理,维持了对RealSearcher对象、AccessValidator对象和Logger对象的引用。
*/
public class ProxySearcher implements Searcher{
private RealSearcher realSearcher = new RealSearcher();
private AccessValidator accessValidator;
private Logger logger;
@Override
public String doSearch(String userId, String keyword) {
if (this.validate(userId)) {
String result = realSearcher.doSearch(userId, keyword);
this.log(userId);
return result;
} else {
return "用户访问受限!";
}
}
//创建访问验证对象并调用其Validate()方法实现身份验证
public boolean validate(String userId) {
accessValidator = new AccessValidator();
return accessValidator.validate(userId);
}
//创建日志记录对象并调用其Log()方法实现日志记录
public void log(String userId) {
logger = new Logger();
logger.log(userId);
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
Searcher searcher = new ProxySearcher();
String result = searcher.doSearch("杨过", "玉女心经");
}
}
运行结果如下
在数据库中验证用户'杨过'是否是合法用户
杨过登录成功
用户'杨过'使用关键词'玉女心经'查询商务信息!
更新数据库,用户'杨过'查询次数加1!
本实例是保护代理和智能引用代理的应用实例,在代理类ProxySearcher中实现对真实主题类的权限控制和引用计数,如果需要在访问真实主题时增加新的访问控制机制和新功能,添加一个新的代理类即可,客户端代码该用新的代理类实例化
**远程代理(Remote Proxy)**是一种常用的代理模式,它使得客户端程序可以访问在远程主机上的对象,远程主机可能具有更好的计算性能与处理速度,可以快速响应并处理客户端的请求。远程代理可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户端完全可以认为被代理的远程业务对象是在本地而不是在远程,而远程代理对象承担了大部分的网络通信工作,并负责对远程业务方法的调用。
远程代理示意图如图所示,客户端对象不能直接访问远程主机中的业务对象,只能采取间接访问的方式。远程业务对象在本地主机中有一个代理对象,该代理对象负责对远程业务对象的访问和网络通信,它对于客户端对象而言是透明的。客户端无须关心实现具体业务的是谁,只需要按照服务接口所定义的方式直接与本地主机中的代理对象交互即可。
远程代理
**虚拟代理(Virtual Proxy)**也是一种常用的代理模式,对于一些占用系统资源较多或者加载时间较长的对象,可以给这些对象提供一个虚拟代理。在真实对象创建成功之前虚拟代理扮演真实对象的替身,而当真实对象创建之后,虚拟代理将用户的请求转发给真实对象。
通常,在以下两种情况下可以考虑使用虚拟代理:
(1) 由于对象本身的复杂性或者网络等原因导致一个对象需要较长的加载时间,此时可以用一个加载时间相对较短的代理对象来代表真实对象。通常在实现时可以结合多线程技术,一个线程用于显示代理对象,其他线程用于加载真实对象。这种虚拟代理模式可以应用在程序启动的时候,由于创建代理对象在时间和处理复杂度上要少于创建真实对象,因此,在程序启动时,可以用代理对象代替真实对象初始化,大大加速了系统的启动时间。当需要使用真实对象时,再通过代理对象来引用,而此时真实对象可能已经成功加载完毕,可以缩短用户的等待时间。
(2) 当一个对象的加载十分耗费系统资源的时候,也非常适合使用虚拟代理。虚拟代理可以让那些占用大量内存或处理起来非常复杂的对象推迟到使用它们的时候才创建,而在此之前用一个相对来说占用资源较少的代理对象来代表真实对象,再通过代理对象来引用真实对象。为了节省内存,在第一次引用真实对象时再创建对象,并且该对象可被多次重用,在以后每次访问时需要检测所需对象是否已经被创建,因此在访问该对象时需要进行存在性检测,这需要消耗一定的系统时间,但是可以节省内存空间,这是一种用时间换取空间的做法。
无论是以上哪种情况,虚拟代理都是用一个“虚假”的代理对象来代表真实对象,通过代理对象来间接引用真实对象,可以在一定程度上提高系统的性能。
**缓冲代理(Cache Proxy)**也是一种较为常用的代理模式,它为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,从而可以避免某些方法的重复执行,优化系统性能。
在微软示例项目PetShop 4.0的业务逻辑层(Business Logic Layer, BLL)中定义了Product、Category、Item等类,它们封装了相关的业务方法,用于调用数据访问层(Data Access Layer, DAL)对象访问数据库,以获取相关数据。为了改进系统性能,PetShop 4.0为这些实现方法增加缓存机制,引入一个新的对象去控制原来的BLL业务逻辑对象,这些新的对象对应于代理模式中的代理对象。在引入代理模式后,实现了在缓存级别上对业务对象的封装,增强了对业务对象的控制,如果需要访问的数据在缓存中已经存在,则无须再重复执行获取数据的方法,直接返回存储在缓存中的数据即可。由于原有业务对象(真实对象)和新增代理对象暴露在外的方法是一致的,因而对于调用方即客户端而言,调用代理对象与真实对象并没有实质的区别。
这些新引入的代理类包括ProductDataProxy、CategoryDataProxy和ItemDataProxy等。下面以PetShop.BLL.Product业务对象为例进行说明,PetShop 4.0为其建立了代理对象ProductDataProxy,并在ProductDataProxy的GetProductsByCategory()方法中调用了业务逻辑层Product类的GetProductsByCategory()方法,同时增加了缓存机制。

PetShop4.0缓存代理示意图
在ProductDataProxy类中存在如下代码片段:
public static class ProductDataProxy
{
private static readonly int productTimeout = int.Parse(ConfigurationManager.AppSettings ["ProductCacheDuration"]);
private static readonly bool enableCaching = bool.Parse(ConfigurationManager. AppSettings["EnableCaching"]);
public static IList GetProductsByCategory(string category)
{
Product product = new Product();
//如果缓存被禁用,则直接通过product对象来获取数据
if (!enableCaching)
{
return product.GetProductsByCategory(category);
}
string key = "product_by_category_" + category;
//从缓存中获取数据
IList data = (IList )HttpRuntime.Cache[key];
//如果缓存中没有数据则执行如下代码
if (data == null)
{
data = product.GetProductsByCategory(category);
//通过工厂创建AggregateCacheDependency对象
AggregateCacheDependency cd = DependencyFacade.GetProductDependency ();
//将数据存储在缓存中,并添加必要的AggregateCacheDependency对象
HttpRuntime.Cache.Add(key, data, cd, DateTime.Now.AddHours(product Timeout), Cache.NoSlidingExpiration, CacheItemPriority.High, null);
}
return data;
}
……
}
在上述代码中,AggregateCacheDependency是从.NET Framework 2.0开始新增的一个类,它负责监视依赖项对象的集合。
当这个集合中的任意一个依赖项对象发生改变时,该依赖项对象对应的缓存对象都将被自动移除。
在此不对AggregateCacheDependency进行详细说明,大家可以查阅相关资料进行扩展学习。
与业务逻辑层Product对象的GetProductsByCategory()方法相比,上述代码增加了缓存机制。
当缓存内不存在相关数据项时,则直接调用业务逻辑层Product的GetProductsByCategory()方法来获取数据,并将其与对应的AggregateCacheDependency对象一起存储在缓存中。
在ProductDataProxy类的每一个业务方法中都实例化了Product类,再调用Product类的相应方法,因此ProductDataProxy与Product之间属于依赖关系,这是标准代理模式的一种变形,可以按照标准代理模式对其进行改进,包括引入高层的抽象接口。
代理模式是常用的结构型设计模式之一,它为对象的间接访问提供了一个解决方案,可以对对象的访问进行控制。代理模式类型较多,其中远程代理、虚拟代理、保护代理等在软件开发中应用非常广泛。
代理模式的共同优点如下:
(1) 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
(2) 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。
此外,不同类型的代理模式也具有独特的优点,例如:
(1) 远程代理为位于两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高系统的整体运行效率。
(2) 虚拟代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销。
(3) 缓冲代理为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间。
(4) 保护代理可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限。
代理模式的主要缺点如下:
(1) 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,例如保护代理。
(2) 实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂,例如远程代理。
代理模式的类型较多,不同类型的代理模式有不同的优缺点,它们应用于不同的场合:
(1) 当客户端对象需要访问远程主机中的对象时可以使用远程代理。
(2) 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理,例如一个对象需要很长时间才能完成加载时。
(3) 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理。通过使用缓冲代理,系统无须在客户端每一次访问时都重新执行操作,只需直接从临时缓冲区获取操作结果即可。
(4) 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理。
(5) 当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理。
很多情况下,在一个软件系统中可以处理某个请求的对象不止一个,例如SCM系统中的采购单审批,主任、副董事长、董事长和董事会都可以处理采购单,他们可以构成一条处理采购单的链式结构,采购单沿着这条链进行传递,这条链就称为职责链。职责链可以是一条直线、一个环或者一个树形结构,最常见的职责链是直线型,即沿着一条单向的链来传递请求。链上的每一个对象都是请求处理者,职责链模式可以将请求的处理者组织成一条链,并让请求沿着链传递,由链上的处理者对请求进行相应的处理,客户端无须关心请求的处理细节以及请求的传递,只需将请求发送到链上即可,实现请求发送者和请求处理者解耦。
职责链模式(Chain of Responsibility Pattern):避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。

在职责链模式结构图中包含如下几个角色:
● Handler**(抽象处理者)**:它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。因为每一个处理者的下家还是一个处理者,因此在抽象处理者中定义了一个抽象处理者类型的对象(如结构图中的successor),作为其对下家的引用。通过该引用,处理者可以连成一条链。
● ConcreteHandler(具体处理者):它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中下一个对象,以便请求的转发。
在职责链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任。
abstract class Handler {
//维持对下家的引用
protected Handler successor;
public void setSuccessor(Handler successor) {
this.successor=successor;
}
public abstract void handleRequest(String request);
}
上述代码中,抽象处理者类定义了对下家的引用对象,以便将请求转发给下家,该对象的访问符可设为protected,在其子类中可以使用。在抽象处理者类中声明了抽象的请求处理方法,具体实现交由子类完成。
具体处理者是抽象处理者的子类,它具有两大作用:
第一是处理请求,不同的具体处理者以不同的形式实现抽象请求处理方法handleRequest();
第二是转发请求,如果该请求超出了当前处理者类的权限,可以将该请求转发给下家。具体处理者类的典型代码如下:
class ConcreteHandler extends Handler {
public void handleRequest(String request) {
if (请求满足条件) {
//处理请求
}
else {
this.successor.handleRequest(request); //转发请求
}
}
}
在具体处理类中通过对请求进行判断可以做出相应的处理。
需要注意的是, 职责链模式并不创建职责链,职责链的创建工作必须由系统的其他部分来完成,一般是在使用该职责链的客户端中创建职责链。
职责链模式降低了请求的发送端和接收端之间的耦合,使多个对象都有机会处理这个请求。
Sunny软件公司承接了某企业SCM(Supply Chain Management,供应链管理)系统的开发任务,其中包含一个采购审批子系统。该企业的采购审批是分级进行的,即根据采购金额的不同由不同层次的主管人员来审批,主任可以审批5万元以下(不包括5万元)的采购单,副董事长可以审批5万元至10万元(不包括10万元)的采购单,董事长可以审批10万元至50万元(不包括50万元)的采购单,50万元及以上的采购单就需要开董事会讨论决定。如图

如何在软件中实现采购单的分级审批?Sunny软件公司开发人员提出了一个初始解决方案,在系统中提供一个采购单处理类PurchaseRequestHandler用于统一处理采购单,其框架代码如下所示:
//采购单处理类
class PurchaseRequestHandler {
//递交采购单给主任
public void sendRequestToDirector(PurchaseRequest request) {
if (request.getAmount() < 50000) {
//主任可审批该采购单
this.handleByDirector(request);
}
else if (request.getAmount() < 100000) {
//副董事长可审批该采购单
this.handleByVicePresident(request);
}
else if (request.getAmount() < 500000) {
//董事长可审批该采购单
this.handleByPresident(request);
}
else {
//董事会可审批该采购单
this.handleByCongress(request);
}
}
//主任审批采购单
public void handleByDirector(PurchaseRequest request) {
//代码省略
}
//副董事长审批采购单
public void handleByVicePresident(PurchaseRequest request) {
//代码省略
}
//董事长审批采购单
public void handleByPresident(PurchaseRequest request) {
//代码省略
}
//董事会审批采购单
public void handleByCongress(PurchaseRequest request) {
//代码省略
}
}
问题貌似很简单,但仔细分析,发现上述方案存在如下几个问题:
(1)PurchaseRequestHandler类较为庞大,各个级别的审批方法都集中在一个类中,违反了“单一职责原则”,测试和维护难度大。
(2)如果需要增加一个新的审批级别或调整任何一级的审批金额和审批细节(例如将董事长的审批额度改为60万元)时都必须修改源代码并进行严格测试,此外,如果需要移除某一级别(例如金额为10万元及以上的采购单直接由董事长审批,不再设副董事长一职)时也必须对源代码进行修改,违反了“开闭原则”。
(3)审批流程的设置缺乏灵活性,现在的审批流程是“主任–>副董事长–>董事长–>董事会”,如果需要改为“主任–>董事长–>董事会”,在此方案中只能通过修改源代码来实现,客户端无法定制审批流程。
如何针对上述问题对系统进行改进?Sunny公司开发人员迫切需要一种新的设计方案,还好有职责链模式,通过使用职责链模式我们可以最大程度地解决这些问题
为了让采购单的审批流程更加灵活,并实现采购单的链式传递和处理,Sunny公司开发人员使用职责链模式来实现采购单的分级审批,其基本结构如图

抽象类Approver充当抽象处理者(抽象传递者),Director、VicePresident、President和Congress充当具体处理者(具体传递者),PurchaseRequest充当请求类。完整代码如下所示:
import lombok.AllArgsConstructor;
import lombok.Data;
//采购单:请求类
@AllArgsConstructor
@Data
class PurchaseRequest {
private double amount; //采购金额
private int number; //采购单编号
private String purpose; //采购目的
}
import lombok.Setter;
//审批者类:抽象处理者
abstract class Approver {
//设置后继者
@Setter
protected Approver successor; //定义后继对象
protected String name; //审批者姓名
public Approver(String name) {
this.name = name;
}
//抽象请求处理方法
public abstract void processRequest(PurchaseRequest request);
}
//主任类:具体处理者
class Director extends Approver {
public Director(String name) {
super(name);
}
//具体请求处理方法
public void processRequest(PurchaseRequest request) {
if (request.getAmount() < 50000) {
System.out.println("主任" + this.name + "审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:" + request.getPurpose() + "。"); //处理请求
}
else {
System.out.println("主任" + this.name + "转发请求给" + this.successor.name);
this.successor.processRequest(request); //转发请求
}
}
}
//副董事长类:具体处理者
class VicePresident extends Approver {
public VicePresident(String name) {
super(name);
}
//具体请求处理方法
public void processRequest(PurchaseRequest request) {
if (request.getAmount() < 100000) {
System.out.println("副董事长" + this.name + "审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:" + request.getPurpose() + "。"); //处理请求
}
else {
System.out.println("副董事长" + this.name + "转发请求给" + this.successor.name);
this.successor.processRequest(request); //转发请求
}
}
}
//董事长类:具体处理者
class President extends Approver {
public President(String name) {
super(name);
}
//具体请求处理方法
public void processRequest(PurchaseRequest request) {
if (request.getAmount() < 500000) {
System.out.println("董事长" + this.name + "审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:" + request.getPurpose() + "。"); //处理请求
}
else {
System.out.println("董事长" + this.name + "转发请求给" + this.successor.name);
this.successor.processRequest(request); //转发请求
}
}
}
//董事会类:具体处理者
class Congress extends Approver {
public Congress(String name) {
super(name);
}
//具体请求处理方法
public void processRequest(PurchaseRequest request) {
System.out.println("召开董事会审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:" + request.getPurpose() + "。"); //处理请求
}
}
客户端代码
class Client {
public static void main(String[] args) {
Approver wjzhang,gyang,jguo,meeting, rhuang;
wjzhang = new Director("张无忌");
gyang = new VicePresident("杨过");
jguo = new President("郭靖");
meeting = new Congress("董事会");
rhuang = new Manager("黄蓉");
// 创建职责链
wjzhang.setSuccessor(gyang);
gyang.setSuccessor(jguo);
jguo.setSuccessor(meeting);
//创建采购单
PurchaseRequest pr1 = new PurchaseRequest(45000,10001,"购买倚天剑");
wjzhang.processRequest(pr1);
PurchaseRequest pr2 = new PurchaseRequest(60000,10002,"购买《葵花宝典》");
wjzhang.processRequest(pr2);
PurchaseRequest pr3 = new PurchaseRequest(160000,10003,"购买《金刚经》");
wjzhang.processRequest(pr3);
PurchaseRequest pr4 = new PurchaseRequest(800000,10004,"购买桃花岛");
wjzhang.processRequest(pr4);
}
}
输出
主任张无忌审批采购单:10001,金额:45000.0元,采购目的:购买倚天剑。
主任张无忌转发请求给杨过
副董事长杨过审批采购单:10002,金额:60000.0元,采购目的:购买《葵花宝典》。
主任张无忌转发请求给杨过
副董事长杨过转发请求给郭靖
董事长郭靖审批采购单:10003,金额:160000.0元,采购目的:购买《金刚经》。
主任张无忌转发请求给杨过
副董事长杨过转发请求给郭靖
董事长郭靖转发请求给董事会
召开董事会审批采购单:10004,金额:800000.0元,采购目的:购买桃花岛。
如果需要在系统增加一个新的具体处理者,如增加一个经理(Manager)角色可以审批5万元至8万元(不包括8万元)的采购单,需要编写一个新的具体处理者类Manager,作为抽象处理者类Approver的子类,实现在Approver类中定义的抽象处理方法,如果采购金额大于等于8万元,则将请求转发给下家,代码如下所示:
//经理类:具体处理者
class Manager extends Approver {
public Manager(String name) {
super(name);
}
//具体请求处理方法
public void processRequest(PurchaseRequest request) {
if (request.getAmount() < 80000) {
System.out.println("经理" + this.name + "审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:" + request.getPurpose() + "。"); //处理请求
}
else {
System.out.println("经理" + this.name + "转发请求给" + this.successor.name);
this.successor.processRequest(request); //转发请求
}
}
}
由于链的创建过程由客户端负责,因此增加新的具体处理者类对原有类库无任何影响,无须修改已有类的源代码,符合“开闭原则”。
在客户端代码中,如果要将新的具体请求处理者应用在系统中,需要创建新的具体处理者对象,然后将该对象加入职责链中。如在客户端测试代码中增加如下代码:
class Client {
public static void main(String[] args) {
Approver wjzhang,gyang,jguo,meeting, rhuang;
wjzhang = new Director("张无忌");
gyang = new VicePresident("杨过");
jguo = new President("郭靖");
meeting = new Congress("董事会");
rhuang = new Manager("黄蓉");
// 创建职责链
wjzhang.setSuccessor(rhuang);
rhuang.setSuccessor(gyang);
gyang.setSuccessor(jguo);
jguo.setSuccessor(meeting);
//创建采购单
PurchaseRequest pr1 = new PurchaseRequest(45000,10001,"购买倚天剑");
wjzhang.processRequest(pr1);
PurchaseRequest pr2 = new PurchaseRequest(60000,10002,"购买《葵花宝典》");
wjzhang.processRequest(pr2);
PurchaseRequest pr3 = new PurchaseRequest(160000,10003,"购买《金刚经》");
wjzhang.processRequest(pr3);
PurchaseRequest pr4 = new PurchaseRequest(800000,10004,"购买桃花岛");
wjzhang.processRequest(pr4);
}
}
输出
主任张无忌审批采购单:10001,金额:45000.0元,采购目的:购买倚天剑。
主任张无忌转发请求给黄蓉
经理黄蓉审批采购单:10002,金额:60000.0元,采购目的:购买《葵花宝典》。
主任张无忌转发请求给黄蓉
经理黄蓉转发请求给杨过
副董事长杨过转发请求给郭靖
董事长郭靖审批采购单:10003,金额:160000.0元,采购目的:购买《金刚经》。
主任张无忌转发请求给黄蓉
经理黄蓉转发请求给杨过
副董事长杨过转发请求给郭靖
董事长郭靖转发请求给董事会
召开董事会审批采购单:10004,金额:800000.0元,采购目的:购买桃花岛。
一个纯的职责链模式要求一个具体处理者对象只能在两个行为中选择一个:
要么承担全部责任,要么将责任推给下家,不允许出现某一个具体处理者对象在承担了一部分或全部责任后又将责任向下传递的情况。
而且在纯的职责链模式中,要求一个请求必须被某一个处理者对象所接收,不能出现某个请求未被任何一个处理者对象处理的情况。
在前面的采购单审批实例中应用的是纯的职责链模式。
在一个不纯的职责链模式中允许某个请求被一个具体处理者部分处理后再向下传递,或者一个具体处理者处理完某请求后其后继处理者可以继续处理该请求,而且一个请求可以最终不被任何处理者对象所接收。
Java AWT 1.0中的事件处理模型应用的是不纯的职责链模式,其基本原理如下:
由于窗口组件(如按钮、文本框等)一般都位于容器组件中,因此当事件发生在某一个组件上时,先通过组件对象的handleEvent()方法将事件传递给相应的事件处理方法,该事件处理方法将处理此事件,然后决定是否将该事件向上一级容器组件传播;
上级容器组件在接到事件之后可以继续处理此事件并决定是否继续向上级容器组件传播,如此反复,直到事件到达顶层容器组件为止;
如果一直传到最顶层容器仍没有处理方法,则该事件不予处理。
每一级组件在接收到事件时,都可以处理此事件,而不论此事件是否在上一级已得到处理,还存在事件未被处理的情况。
显然,这就是不纯的职责链模式,早期的Java AWT事件模型(JDK 1.0及更早)中的这种事件处理机制又叫事件浮升(Event Bubbling)机制。
从Java.1.1以后,JDK使用观察者模式代替职责链模式来处理事件。
目前,在JavaScript中仍然可以使用这种事件浮升机制来进行事件处理。
职责链模式通过建立一条链来组织请求的处理者,请求将沿着链进行传递,请求发送者无须知道请求在何时、何处以及如何被处理,实现了请求发送者与处理者的解耦。在软件开发中,如果遇到有多个对象可以处理同一请求时可以应用职责链模式,例如在Web应用开发中创建一个过滤器(Filter)链来对请求数据进行过滤,在工作流系统中实现公文的分级审批等等,使用职责链模式可以较好地解决此类问题。
职责链模式的主要优点如下:
(1) 职责链模式使得一个对象无须知道是其他哪一个对象处理其请求,对象仅需知道该请求会被处理即可,接收者和发送者都没有对方的明确信息,且链中的对象不需要知道链的结构,由客户端负责链的创建,降低了系统的耦合度。
(2) 请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象的相互连接。
(3) 在给对象分派职责时,职责链可以给我们更多的灵活性,可以通过在运行时对该链进行动态的增加或修改来增加或改变处理一个请求的职责。
(4) 在系统中增加一个新的具体请求处理者时无须修改原有系统的代码,只需要在客户端重新建链即可,从这一点来看是符合“开闭原则”的。
### 缺点
职责链模式的主要缺点如下:
(1) 由于一个请求没有明确的接收者,那么就不能保证它一定会被处理,该请求可能一直到链的末端都得不到处理;一个请求也可能因职责链没有被正确配置而得不到处理。
(2) 对于比较长的职责链,请求的处理可能涉及到多个处理对象,系统性能将受到一定影响,而且在进行代码调试时不太方便。
(3) 如果建链不当,可能会造成循环调用,将导致系统陷入死循环。
在以下情况下可以考虑使用职责链模式:
(1) 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定,客户端只需将请求提交到链上,而无须关心请求的处理对象是谁以及它是如何处理的。
(2) 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
(3) 可动态指定一组对象处理请求,客户端可以动态创建职责链来处理请求,还可以改变链中处理者之间的先后次序。
装修新房的最后几道工序之一是安装插座和开关,通过开关可以控制一些电器的打开和关闭,例如电灯或者排气扇。
在购买开关时,我们并不知道它将来到底用于控制什么电器,也就是说,开关与电灯、排气扇并无直接关系,一个开关在安装之后可能用来控制电灯,也可能用来控制排气扇或者其他电器设备。
开关与电器之间通过电线建立连接,如果开关打开,则电线通电,电器工作;
反之,开关关闭,电线断电,电器停止工作。相同的开关可以通过不同的电线来控制不同的电器。
我们可以将开关理解成一个请求的发送者,用户通过它来发送一个“开灯”请求,而电灯是“开灯”请求的最终接收者和处理者。
开关和电灯之间并不存在直接耦合关系,它们通过电线连接在一起,使用不同的电线可以连接不同的请求接收者,只需更换一根电线,相同的发送者(开关)即可对应不同的接收者(电器)。
在软件开发中也存在很多与开关和电器类似的请求发送者和接收者对象,例如一个按钮,它可能是一个“关闭窗口”请求的发送者,而按钮点击事件处理类则是该请求的接收者。
为了降低系统的耦合度,将请求的发送者和接收者解耦,我们可以使用一种被称之为命令模式的设计模式来设计系统,在命令模式中,发送者与接收者之间引入了新的命令对象(类似图1中的电线),将发送者的请求封装在命令对象中,再通过命令对象来调用接收者的方法。本章我们将学习用于将请求发送者和接收者解耦的命令模式。
在软件开发中,我们经常需要向某些对象发送请求(调用其中的某个或某些方法),但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,此时,我们特别希望能够以一种松耦合的方式来设计软件,使得请求发送者与请求接收者能够消除彼此之间的耦合,让对象之间的调用关系更加灵活,可以灵活地指定请求接收者以及被请求的操作。命令模式为此类问题提供了一个较为完美的解决方案。
命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。
命令模式(Command Pattern):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。

● Command(抽象命令类):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。
● ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。
● Invoker(调用者):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。
● Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。
命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开。每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行相应的操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。
**命令模式的关键在于引入了抽象命令类,请求发送者针对抽象命令类编程,只有实现了抽象命令类的具体命令才与请求接收者相关联。**在最简单的抽象命令类中只包含了一个抽象的execute()方法,每个具体命令类将一个Receiver类型的对象作为一个实例变量进行存储,从而具体指定一个请求的接收者,不同的具体命令类提供了execute()方法的不同实现,并调用不同接收者的请求处理方法。
abstract class Command {
public abstract void execute();
}
对于请求发送者即调用者而言,将针对抽象命令类进行编程,可以通过构造注入或者设值注入的方式在运行时传入具体命令类对象,并在业务方法中调用命令对象的execute()方法
class Invoker {
private Command command;
//构造注入
public Invoker(Command command) {
this.command = command;
}
//设值注入
public void setCommand(Command command) {
this.command = command;
}
//业务方法,用于调用命令类的execute()方法
public void call() {
command.execute();
}
}
具体命令类继承了抽象命令类,它与请求接收者相关联,实现了在抽象命令类中声明的execute()方法,并在实现时调用接收者的请求响应方法action()
class ConcreteCommand extends Command {
private Receiver receiver; //维持一个对请求接收者对象的引用
public void execute() {
receiver.action(); //调用请求接收者的业务处理方法action()
}
}
请求接收者Receiver类具体实现对请求的业务处理,它提供了action()方法,用于执行与请求相关的操作
class Receiver {
public void action() {
//具体操作
}
}
Sunny软件公司开发人员为公司内部OA系统开发了一个桌面版应用程序,该应用程序为用户提供了一系列自定义功能键,用户可以通过这些功能键来实现一些快捷操作。
Sunny软件公司开发人员通过分析,发现不同的用户可能会有不同的使用习惯,在设置功能键的时候每个人都有自己的喜好,例如有的人喜欢将第一个功能键设置为“打开帮助文档”,有的人则喜欢将该功能键设置为“最小化至托盘”。
为了让用户能够灵活地进行功能键的设置,开发人员提供了一个“功能键设置”窗口,用户可以将功能键和相应功能绑定在一起,还可以根据需要来修改功能键的设置,而且系统在未来可能还会增加一些新的功能或功能键。
//FunctionButton:功能键类,请求发送者
class FunctionButton {
private HelpHandler help; //HelpHandler:帮助文档处理类,请求接收者
//在FunctionButton的onClick()方法中调用HelpHandler的display()方法
public void onClick() {
help = new HelpHandler();
help.display(); //显示帮助文档
}
}
在上述代码中,功能键类FunctionButton充当请求的发送者,帮助文档处理类HelpHandler充当请求的接收者,在发送者FunctionButton的onClick()方法中将调用接收者HelpHandler的display()方法。
显然,如果使用上述代码,将给系统带来如下几个问题:
(1) 由于请求发送者和请求接收者之间存在方法的直接调用,耦合度很高,更换请求接收者必须修改发送者的源代码,如果需要将请求接收者HelpHandler改为WindowHanlder(窗口处理类),则需要修改FunctionButton的源代码,违背了“开闭原则”。
(2) FunctionButton类在设计和实现时功能已被固定,如果增加一个新的请求接收者,如果不修改原有的FunctionButton类,则必须增加一个新的与FunctionButton功能类似的类,这将导致系统中类的个数急剧增加。由于请求接收者HelpHandler、WindowHanlder等类之间可能不存在任何关系,它们没有共同的抽象层,因此也很难依据“依赖倒转原则”来设计FunctionButton。
(3) 用户无法按照自己的需要来设置某个功能键的功能,一个功能键类的功能一旦固定,在不修改源代码的情况下无法更换其功能,系统缺乏灵活性。
不难得知,所有这些问题的产生都是因为请求发送者FunctionButton类和请求接收者HelpHandler、WindowHanlder等类之间存在直接耦合关系,如何降低请求发送者和接收者之间的耦合度,让相同的发送者可以对应不同的接收者?这是Sunny软件公司开发人员在设计“功能键设置”模块时不得不考虑的问题。命令模式正为解决这类问题而诞生,此时,如果我们使用命令模式,可以在一定程度上解决上述问题(注:命令模式无法解决类的个数增加的问题)
FBSettingWindow是“功能键设置”界面类,FunctionButton充当请求调用者,Command充当抽象命令类,MinimizeCommand和HelpCommand充当具体命令类,WindowHanlder和HelpHandler充当请求接收者。
//抽象命令类
abstract class Command {
public abstract void execute();
}
//帮助命令类:具体命令类
class HelpCommand extends Command {
private final HelpHandler hhObj; //维持对请求接收者的引用
public HelpCommand() {
hhObj = new HelpHandler();
}
//命令执行方法,将调用请求接收者的业务方法
public void execute() {
hhObj.display();
}
}
//最小化命令类:具体命令类
class MinimizeCommand extends Command {
private final WindowHanlder whObj; //维持对请求接收者的引用
public MinimizeCommand() {
whObj = new WindowHanlder();
}
//命令执行方法,将调用请求接收者的业务方法
public void execute() {
whObj.minimize();
}
}
import lombok.Getter;
import java.util.ArrayList;
//功能键设置窗口类
class FBSettingWindow {
@Getter
private String title; //窗口标题
//定义一个ArrayList来存储所有功能键
private final ArrayList<FunctionButton> functionButtons = new ArrayList<FunctionButton>();
public FBSettingWindow(String title) {
this.title = title;
}
public void addFunctionButton(FunctionButton fb) {
functionButtons.add(fb);
}
public void removeFunctionButton(FunctionButton fb) {
functionButtons.remove(fb);
}
//显示窗口及功能键
public void display() {
System.out.println("显示窗口:" + this.title);
System.out.println("显示功能键:");
for (FunctionButton obj : functionButtons) {
System.out.println(obj.getName());
}
System.out.println("------------------------------");
}
}
import lombok.Getter;
import lombok.Setter;
//功能键类:请求发送者
class FunctionButton {
@Getter
private String name; //功能键名称
//为功能键注入命令
@Setter
private Command command; //维持一个抽象命令对象的引用
public FunctionButton(String name) {
this.name = name;
}
//发送请求的方法
public void onClick() {
System.out.print("点击功能键:");
command.execute();
}
}
//帮助文档处理类:请求接收者
class HelpHandler {
public void display() {
System.out.println("显示帮助文档!");
}
}
//窗口处理类:请求接收者
class WindowHanlder {
public void minimize() {
System.out.println("将窗口最小化至托盘!");
}
}
客户端代码
class Client {
public static void main(String args[]) {
FBSettingWindow fbsw = new FBSettingWindow("功能键设置");
FunctionButton fb1,fb2;
fb1 = new FunctionButton("功能键1");
fb2 = new FunctionButton("功能键1");
Command command1,command2;
//通过读取配置文件和反射生成具体命令对象
command1 = new HelpCommand();
command2 = new MinimizeCommand();
//将命令对象注入功能键
fb1.setCommand(command1);
fb2.setCommand(command2);
fbsw.addFunctionButton(fb1);
fbsw.addFunctionButton(fb2);
fbsw.display();
//调用功能键的业务方法
fb1.onClick();
fb2.onClick();
}
}
执行结果
显示窗口:功能键设置
显示功能键:
功能键1
功能键1
------------------------------
点击功能键:显示帮助文档!
点击功能键:将窗口最小化至托盘!
如果需要修改功能键的功能,例如某个功能键可以实现“自动截屏”,只需要对应增加一个新的具体命令类,在该命令类与屏幕处理者(ScreenHandler)之间创建一个关联关系,然后将该具体命令类的对象通过配置文件注入到某个功能键即可,原有代码无须修改,符合“开闭原则”。在此过程中,每一个具体命令类对应一个请求的处理者(接收者),通过向请求发送者注入不同的具体命令对象可以使得相同的发送者对应不同的接收者,从而实现“将一个请求封装为一个对象,用不同的请求对客户进行参数化”,客户端只需要将具体命令对象作为参数注入请求发送者,无须直接操作请求的接收者。
有时候我们需要将多个请求排队,当一个请求发送者发送一个请求时,将不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理。此时,我们可以通过命令队列来实现。
命令队列的实现方法有多种形式,其中最常用、灵活性最好的一种方式是增加一个CommandQueue类,由该类来负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者,CommandQueue类的典型代码如下所示:
import java.util.*;
class CommandQueue {
//定义一个ArrayList来存储命令队列
private ArrayList<Command> commands = new ArrayList<Command>();
public void addCommand(Command command) {
commands.add(command);
}
public void removeCommand(Command command) {
commands.remove(command);
}
//循环调用每一个命令对象的execute()方法
public void execute() {
for (Object command : commands) {
((Command)command).execute();
}
}
}
在增加了命令队列类CommandQueue以后,请求发送者类Invoker将针对CommandQueue编程,代码修改如下
class Invoker {
private CommandQueue commandQueue; //维持一个CommandQueue对象的引用
//构造注入
public Invoker(CommandQueue commandQueue) {
this. commandQueue = commandQueue;
}
//设值注入
public void setCommandQueue(CommandQueue commandQueue) {
this.commandQueue = commandQueue;
}
//调用CommandQueue类的execute()方法
public void call() {
commandQueue.execute();
}
}
命令队列与我们常说的“批处理”有点类似。批处理,顾名思义,可以对一组对象(命令)进行批量处理,当一个发送者发送请求后,将有一系列接收者对请求作出响应,命令队列可以用于设计批处理应用程序,如果请求接收者的接收次序没有严格的先后次序,我们还可以使用多线程技术来并发调用命令对象的execute()方法,从而提高程序的执行效率。
还是功能按键设置,这里模拟了一个安装多个软件 先安装360 再安装 3721的按钮队列。完整代码
//抽象命令类
abstract class Command {
public abstract void installSoftware();
}
import java.util.ArrayList;
class CommandQueue {
//定义一个ArrayList来存储命令队列
private final ArrayList<Command> commands = new ArrayList<Command>();
public void addCommand(Command command) {
commands.add(command);
}
public void removeCommand(Command command) {
commands.remove(command);
}
//循环调用每一个命令对象的execute()方法
public void execute() {
for (Command command : commands) {
command.installSoftware();
}
}
}
//帮助命令类:具体命令类
class Soft360Command extends Command {
private final Soft360Installer soft360Installer;
public Soft360Command() {
soft360Installer = new Soft360Installer();
}
@Override
public void installSoftware() {
soft360Installer.install();
}
}
//帮助文档处理类:请求接收者
class Soft360Installer {
public void install() {
System.out.println("安装360!");
}
}
//帮助命令类:具体命令类
class Soft3721Command extends Command {
private final Soft3721Installer soft3721Installer;
public Soft3721Command() {
soft3721Installer = new Soft3721Installer();
}
@Override
public void installSoftware() {
soft3721Installer.install();
}
}
//帮助文档处理类:请求接收者
class Soft3721Installer {
public void install() {
System.out.println("安装3721!");
}
}
import lombok.Getter;
import java.util.ArrayList;
//功能键设置窗口类
class FBSettingWindow {
@Getter
private String title; //窗口标题
//定义一个ArrayList来存储所有功能键
private final ArrayList<FunctionButton> functionButtons = new ArrayList<FunctionButton>();
public FBSettingWindow(String title) {
this.title = title;
}
public void addFunctionButton(FunctionButton fb) {
functionButtons.add(fb);
}
public void removeFunctionButton(FunctionButton fb) {
functionButtons.remove(fb);
}
//显示窗口及功能键
public void display() {
System.out.println("显示窗口:" + this.title);
System.out.println("显示功能键:");
for (FunctionButton obj : functionButtons) {
System.out.println(obj.getName());
}
System.out.println("------------------------------");
}
}
import lombok.Getter;
import lombok.Setter;
//功能键类:请求发送者
class FunctionButton {
@Getter
private String name; //功能键名称
//为功能键注入命令
@Setter
private CommandQueue commandQueue; //维持一个抽象命令对象的引用
public FunctionButton(String name) {
this.name = name;
}
//发送请求的方法
public void onClick() {
System.out.println("点击功能键:");
commandQueue.execute();
}
}
在命令模式中,我们可以通过调用一个命令对象的execute()方法来实现对请求的处理,如果需要撤销(Undo)请求,可通过在命令类中增加一个逆向操作来实现。
除了通过一个逆向操作来实现撤销(Undo)外,还可以通过保存对象的历史状态来实现撤销,后者可使用备忘录模式(Memento Pattern)来实现。
计算器界面类CalculatorForm充当请求发送者,实现了数据求和功能的加法类Adder充当请求接收者,界面类可间接调用加法类中的add()方法实现加法运算,并且提供了可撤销加法运算的undo()方法。
//加法类:请求接收者
class Adder {
private int num=0; //定义初始值为0
//加法操作,每次将传入的值与num作加法运算,再将结果返回
public int add(int value) {
num += value;
return num;
}
}
//抽象命令类
abstract class AbstractCommand {
public abstract int execute(int value); //声明命令执行方法execute()
public abstract int undo(); //声明撤销方法undo()
}
//具体命令类
class ConcreteCommand extends AbstractCommand {
private Adder adder = new Adder();
private int value;
//实现抽象命令类中声明的execute()方法,调用加法类的加法操作
public int execute(int value) {
this.value=value;
return adder.add(value);
}
//实现抽象命令类中声明的undo()方法,通过加一个相反数来实现加法的逆向操作
public int undo() {
return adder.add(-value);
}
}
//计算器界面类:请求发送者
class CalculatorForm {
private AbstractCommand command;
public void setCommand(AbstractCommand command) {
this.command = command;
}
//调用命令对象的execute()方法执行运算
public void compute(int value) {
int i = command.execute(value);
System.out.println("执行运算,运算结果为:" + i);
}
//调用命令对象的undo()方法执行撤销
public void undo() {
int i = command.undo();
System.out.println("执行撤销,运算结果为:" + i);
}
}
客户端代码
class Client {
public static void main(String args[]) {
CalculatorForm form = new CalculatorForm();
AbstractCommand command;
command = new ConcreteCommand();
form.setCommand(command); //向发送者注入命令对象
form.compute(10);
form.compute(5);
form.compute(10);
form.undo();
}
}
输出
执行运算,运算结果为:10
执行运算,运算结果为:15
执行运算,运算结果为:25
**执行撤销,运算结果为:15**
需要注意的是在本实例中只能实现一步撤销操作,因为没有保存命令对象的历史状态,可以通过引入一个命令集合或其他方式来存储每一次操作时命令的状态,从而实现多次撤销操作。除了Undo操作外,还可以采用类似的方式实现恢复(Redo)操作,即恢复所撤销的操作(或称为二次撤销)。
如何实现多次undo 和 redo 操作呢。
请求日志就是将请求的历史记录保存下来,通常以**日志文件(Log File)**的形式永久存储在计算机中。很多系统都提供了日志文件,例如Windows日志文件、Oracle日志文件等,日志文件可以记录用户对系统的一些操作(例如对数据的更改)。请求日志文件可以实现很多功能,常用功能如下:
(1) “天有不测风云”,一旦系统发生故障,日志文件可以为系统提供一种恢复机制,在请求日志文件中可以记录用户对系统的每一步操作,从而让系统能够顺利恢复到某一个特定的状态;
(2) 请求日志也可以用于实现批处理,在一个请求日志文件中可以存储一系列命令对象,例如一个命令队列;
(3) 可以将命令队列中的所有命令对象都存储在一个日志文件中,每执行一个命令则从日志文件中删除一个对应的命令对象,防止因为断电或者系统重启等原因造成请求丢失,而且可以避免重新发送全部请求时造成某些命令的重复执行,只需读取请求日志文件,再继续执行文件中剩余的命令即可。
在实现请求日志时,我们可以将命令对象通过序列化写到日志文件中,此时命令类必须实现java.io.Serializable接口。下面我们通过一个简单实例来说明日志文件的用途以及如何实现请求日志:
Sunny软件公司开发了一个网站配置文件管理工具,可以通过一个可视化界面对网站配置文件进行增删改等操作,该工具使用命令模式进行设计
现在Sunny软件公司开发人员希望将对配置文件的操作请求记录在日志文件中,如果网站重新部署,只需要执行保存在日志文件中的命令对象即可修改配置文件。
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
//抽象命令类,由于需要将命令对象写入文件,因此它实现了Serializable接口
abstract class Command implements Serializable {
@Setter
@Getter
protected String name; //命令名称
protected String args; //命令参数
@Setter
protected ConfigOperator configOperator; //维持对接收者对象的引用
public Command(String name) {
this.name = name;
}
//声明两个抽象的执行方法execute()
public abstract void execute(String args);
public abstract void execute();
}
//增加命令类:具体命令
class InsertCommand extends Command {
public InsertCommand(String name) {
super(name);
}
public void execute(String args) {
this.args = args;
configOperator.insert(args);
}
public void execute() {
configOperator.insert(this.args);
}
}
//修改命令类:具体命令
class ModifyCommand extends Command {
public ModifyCommand(String name) {
super(name);
}
public void execute(String args) {
this.args = args;
configOperator.modify(args);
}
public void execute() {
configOperator.modify(this.args);
}
}
//省略了删除命令类DeleteCommand
import java.io.Serializable;
//配置文件操作类:请求接收者。由于ConfigOperator类的对象是Command的成员对象,它也将随Command对象一起写入文件,因此ConfigOperator也需要实现Serializable接口
class ConfigOperator implements Serializable {
public void insert(String args) {
System.out.println("增加新节点:" + args);
}
public void modify(String args) {
System.out.println("修改节点:" + args);
}
public void delete(String args) {
System.out.println("删除节点:" + args);
}
}
import lombok.Setter;
import java.util.ArrayList;
//配置文件设置窗口类:请求发送者
class ConfigSettingWindow {
//定义一个集合来存储每一次操作时的命令对象
private final ArrayList<Command> commands = new ArrayList<>();
//注入具体命令对象
@Setter
private Command command;
//执行配置文件修改命令,同时将命令对象添加到命令集合中
public void call(String args) {
command.execute(args);
commands.add(command);
}
//记录请求日志,生成日志文件,将命令集合写入日志文件
public void save() {
FileUtil.writeCommands(commands);
}
//从日志文件中提取命令集合,并循环调用每一个命令对象的execute()方法来实现配置文件的重新设置
public void recover() {
ArrayList list;
list = FileUtil.readCommands();
if (list != null) {
for (Object obj : list) {
((Command)obj).execute();
}
}
}
}
import java.io.*;
import java.util.ArrayList;
//工具类:文件操作类
class FileUtil {
//将命令集合写入日志文件
public static void writeCommands(ArrayList commands) {
try {
FileOutputStream file = new FileOutputStream("config.log");
//创建对象输出流用于将对象写入到文件中
ObjectOutputStream objout = new ObjectOutputStream(new BufferedOutputStream(file));
//将对象写入文件
objout.writeObject(commands);
objout.close();
}
catch(Exception e) {
System.out.println("命令保存失败!");
e.printStackTrace();
}
}
//从日志文件中提取命令集合
public static ArrayList readCommands() {
try {
FileInputStream file = new FileInputStream("config.log");
//创建对象输入流用于从文件中读取对象
ObjectInputStream objin = new ObjectInputStream(new BufferedInputStream(file));
//将文件中的对象读出并转换为ArrayList类型
ArrayList commands = (ArrayList)objin.readObject();
objin.close();
return commands;
}
catch(Exception e) {
System.out.println("命令读取失败!");
e.printStackTrace();
return null;
}
}
}
客户端
class Client {
public static void main(String args[]) {
ConfigSettingWindow csw = new ConfigSettingWindow(); //定义请求发送者
Command command; //定义命令对象
ConfigOperator co = new ConfigOperator(); //定义请求接收者
//四次对配置文件的更改
command = new InsertCommand("增加");
command.setConfigOperator(co);
csw.setCommand(command);
csw.call("网站首页");
command = new InsertCommand("增加");
command.setConfigOperator(co);
csw.setCommand(command);
csw.call("端口号");
command = new ModifyCommand("修改");
command.setConfigOperator(co);
csw.setCommand(command);
csw.call("网站首页");
command = new ModifyCommand("修改");
command.setConfigOperator(co);
csw.setCommand(command);
csw.call("端口号");
System.out.println("----------------------------");
System.out.println("保存配置");
csw.save();
System.out.println("----------------------------");
System.out.println("恢复配置");
System.out.println("----------------------------");
csw.recover();
}
}
输出
增加新节点:网站首页
增加新节点:端口号
修改节点:网站首页
修改节点:端口号
----------------------------
保存配置
----------------------------
恢复配置
----------------------------
增加新节点:网站首页
增加新节点:端口号
修改节点:网站首页
修改节点:端口号
宏命令(Macro Command)又称为组合命令,它是组合模式和命令模式联用的产物。
宏命令是一个具体命令类,它拥有一个集合属性,在该集合中包含了对其他命令对象的引用。
通常宏命令不直接与请求接收者交互,而是通过它的成员来调用接收者的方法。
当调用宏命令的execute()方法时,将递归调用它所包含的每个成员命令的execute()方法,一个宏命令的成员可以是简单命令,还可以继续是宏命令。
执行一个宏命令将触发多个具体命令的执行,从而实现对命令的批处理
命令模式是一种使用频率非常高的设计模式,它可以将请求发送者与接收者解耦,请求发送者通过命令对象来间接引用请求接收者,使得系统具有更好的灵活性和可扩展性。在基于GUI的软件开发,无论是在电脑桌面应用还是在移动应用中,命令模式都得到了广泛的应用。
(1) 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。
(2) 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。
(3) 可以比较容易地设计一个命令队列或宏命令(组合命令)。
(4) 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。
使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。
在以下情况下可以考虑使用命令模式:
(1) 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。
(2) 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。
(3) 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
(4) 系统需要将一组操作组合在一起形成宏命令。
虽然目前计算机编程语言有好几百种,但有时候我们还是希望能用一些简单的语言来实现一些特定的操作,我们只要向计算机输入一个句子或文件,它就能够按照预先定义的文法规则来对句子或文件进行解释,从而实现相应的功能。例如提供一个简单的加法/减法解释器,只要输入一个加法/减法表达式,它就能够计算出表达式结果,如图18-1所示,当输入字符串表达式为“1 + 2 + 3 – 4 + 1”时,将输出计算结果为3。

我们知道,像C++、Java和C#等语言无法直接解释类似“1+ 2 + 3 – 4 + 1”这样的字符串(如果直接作为数值表达式时可以解释),我们必须自己定义一套文法规则来实现对这些语句的解释,即设计一个自定义语言。在实际开发中,这些简单的自定义语言可以基于现有的编程语言来设计,如果所基于的编程语言是面向对象语言,此时可以使用解释器模式来实现自定义语言。
“1+ 2 + 3 – 4 + 1”都包含了三个语言单位,可以使用如下文法规则来定义:
expression ::= value | operation
operation ::= expression '+' expression | expression '-' expression
value ::= an integer //一个整数值
该文法规则包含三条语句,第一条表示表达式的组成方式,其中value和operation是后面两个语言单位的定义,每一条语句所定义的字符串如operation和value称为语言构造成分或语言单位,符号“::=”表示“定义为”的意思,其左边的语言单位通过右边来进行说明和定义,语言单位对应终结符表达式和非终结符表达式。如本规则中的operation是非终结符表达式,它的组成元素仍然可以是表达式,可以进一步分解,而value是终结符表达式,它的组成元素是最基本的语言单位,不能再进行分解。
在文法规则定义中可以使用一些符号来表示不同的含义,如使用“|”表示或,使用“{”和“}”表示组合,使用“*”表示出现0次或多次等,其中使用频率最高的符号是表示“或”关系的“|”,如文法规则“boolValue ::= 0 | 1”表示终结符表达式boolValue的取值可以为0或者1。
除了使用文法规则来定义一个语言,在解释器模式中还可以通过一种称之为**抽象语法树(Abstract Syntax Tree, AST)**的图形方式来直观地表示语言的构成,每一棵抽象语法树对应一个语言实例,如加法/减法表达式语言中的语句“1+ 2 + 3 – 4 + 1”,可以通过如下图 的抽象语法树表示

在该抽象语法树中,可以通过终结符表达式value和非终结符表达式operation组成复杂的语句,
每个文法规则的语言实例都可以表示为一个抽象语法树,即每一条具体的语句都可以用类似上图所示的抽象语法树来表示,
在图中终结符表达式类的实例作为树的叶子节点,而非终结符表达式类的实例作为非叶子节点,它们可以将终结符表达式类的实例以及包含终结符和非终结符实例的子表达式作为其子节点。
抽象语法树描述了如何构成一个复杂的句子,通过对抽象语法树的分析,可以识别出语言中的终结符类和非终结符类。
解释器模式描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及如何解释这些句子。
解释器模式是一种使用频率相对较低但学习难度较大的设计模式,它用于描述如何使用面向对象语言构成一个简单的语言解释器。
在某些情况下,为了更好地描述某一些特定类型的问题,我们可以创建一种新的语言,这种语言拥有自己的表达式和结构,即文法规则,这些问题的实例将对应为该语言中的句子。
此时,可以使用解释器模式来设计这种新的语言。对解释器模式的学习能够加深我们对面向对象思想的理解,并且掌握编程语言中文法规则的解释过程。
解释器模式(Interpreter Pattern):定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的“语言”是指使用规定格式和语法的代码。解释器模式是一种类行为型模式。
由于表达式可分为终结符表达式和非终结符表达式,因此解释器模式的结构与组合模式的结构有些类似,但在解释器模式中包含更多的组成元素

● AbstractExpression(抽象表达式):在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类。
● TerminalExpression(终结符表达式):终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。通常在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子。
● NonterminalExpression(非终结符表达式):非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式来完成。
● Context(环境类):环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句。
在解释器模式中,每一种终结符和非终结符都有一个具体类与之对应,正因为使用类来表示每一条文法规则,所以系统将具有较好的灵活性和可扩展性。对于所有的终结符和非终结符,我们首先需要抽象出一个公共父类,即抽象表达式类,其典型代码如下所示:
abstract class AbstractExpression {
public abstract void interpret(Context ctx);
}
终结符表达式和非终结符表达式类都是抽象表达式类的子类,对于终结符表达式,其代码很简单,主要是对终结符元素的处理,其典型代码如下所示:
class TerminalExpression extends AbstractExpression {
public void interpret(Context ctx) {
//终结符表达式的解释操作
}
}
对于非终结符表达式,其代码相对比较复杂,因为可以通过非终结符将表达式组合成更加复杂的结构,对于包含两个操作元素的非终结符表达式类,其典型代码如下:
class NonterminalExpression extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public NonterminalExpression(AbstractExpression left,AbstractExpression right) {
this.left=left;
this.right=right;
}
public void interpret(Context ctx) {
//递归调用每一个组成部分的interpret()方法
//在递归调用时指定组成部分的连接方式,即非终结符的功能
}
}
除了上述用于表示表达式的类以外,通常在解释器模式中还提供了一个环境类Context,用于存储一些全局信息,通常在Context中包含了一个HashMap或ArrayList等类型的集合对象(也可以直接由HashMap等集合类充当环境类),存储一系列公共信息,如变量名与值的映射关系(key/value)等,用于在进行具体的解释操作时从中获取相关信息。其典型代码片段如下:
class Context {
private HashMap map = new HashMap();
public void assign(String key, String value) {
//往环境类中设值
}
public String lookup(String key) {
//获取存储在环境类中的值
}
}
当系统无须提供全局公共信息时可以省略环境类,可根据实际情况决定是否需要环境类。
Sunny软件公司欲为某玩具公司开发一套机器人控制程序,在该机器人控制程序中包含一些简单的英文控制指令,每一个指令对应一个表达式(expression),该表达式可以是简单表达式也可以是复合表达式,每一个简单表达式由移动方向(direction),移动方式(action)和移动距离(distance)三部分组成,其中移动方向包括上(up)、下(down)、左(left)、右(right);移动方式包括移动(move)和快速移动(run);移动距离为一个正整数。两个表达式之间可以通过与(and)连接,形成复合(composite)表达式。
用户通过对图形化的设置界面进行操作可以创建一个机器人控制指令,机器人在收到指令后将按照指令的设置进行移动,例如输入控制指令:up move 5,则“向上移动5个单位”;输入控制指令:down run 10 and left move 20,则“向下快速移动10个单位再向左移动20个单位”。
Sunny软件公司开发人员决定自定义一个简单的语言来解释机器人控制指令,根据上述需求描述,用形式化语言来表示该简单语言的文法规则如下:
expression ::= direction action distance | composite //表达式
composite ::= expression 'and' expression //复合表达式
direction ::= 'up' | 'down' | 'left' | 'right' //移动方向
action ::= 'move' | 'run' //移动方式
distance ::= an integer //移动距离
上述语言一共定义了五条文法规则,对应五个语言单位,这些语言单位可以分为两类,一类为终结符(也称为终结符表达式),例如direction、action和distance,它们是语言的最小组成单位,不能再进行拆分;另一类为非终结符(也称为非终结符表达式),例如expression和composite,它们都是一个完整的句子,包含一系列终结符或非终结符。
我们根据上述规则定义出的语言可以构成很多语句,计算机程序将根据这些语句进行某种操作。为了实现对语句的解释,可以使用解释器模式,在解释器模式中每一个文法规则都将对应一个类,扩展、改变文法以及增加新的文法规则都很方便
为了能够解释机器人控制指令,Sunny软件公司开发人员使用解释器模式来设计和实现机器人控制程序。针对五条文法规则,分别提供五个类来实现,其中终结符表达式direction、action和distance对应DirectionNode类、ActionNode类和DistanceNode类,非终结符表达式expression和composite对应SentenceNode类和AndNode类。
我们可以通过抽象语法树来表示具体解释过程,例如机器人控制指令“down run 10 and left move 20”对应的抽象语法树如图

机器人控制程序实例基本结构如图

AbstractNode充当抽象表达式角色,DirectionNode、ActionNode和DistanceNode充当终结符表达式角色,AndNode和SentenceNode充当非终结符表达式角色。完整代码如下所示:
//注:本实例对机器人控制指令的输出结果进行模拟,将英文指令翻译为中文指令,实际情况是调用不同的控制程序进行机器人的控制,包括对移动方向、方式和距离的控制等
import java.util.*;
//抽象表达式
abstract class AbstractNode {
public abstract String interpret();
}
//And解释:非终结符表达式
class AndNode extends AbstractNode {
private AbstractNode left; //And的左表达式
private AbstractNode right; //And的右表达式
public AndNode(AbstractNode left, AbstractNode right) {
this.left = left;
this.right = right;
}
//And表达式解释操作
public String interpret() {
return left.interpret() + "再" + right.interpret();
}
}
//简单句子解释:非终结符表达式
class SentenceNode extends AbstractNode {
private AbstractNode direction;
private AbstractNode action;
private AbstractNode distance;
public SentenceNode(AbstractNode direction,AbstractNode action,AbstractNode distance) {
this.direction = direction;
this.action = action;
this.distance = distance;
}
//简单句子的解释操作
public String interpret() {
return direction.interpret() + action.interpret() + distance.interpret();
}
}
//方向解释:终结符表达式
class DirectionNode extends AbstractNode {
private String direction;
public DirectionNode(String direction) {
this.direction = direction;
}
//方向表达式的解释操作
public String interpret() {
if (direction.equalsIgnoreCase("up")) {
return "向上";
}
else if (direction.equalsIgnoreCase("down")) {
return "向下";
}
else if (direction.equalsIgnoreCase("left")) {
return "向左";
}
else if (direction.equalsIgnoreCase("right")) {
return "向右";
}
else {
return "无效指令";
}
}
}
//动作解释:终结符表达式
class ActionNode extends AbstractNode {
private String action;
public ActionNode(String action) {
this.action = action;
}
//动作(移动方式)表达式的解释操作
public String interpret() {
if (action.equalsIgnoreCase("move")) {
return "移动";
}
else if (action.equalsIgnoreCase("run")) {
return "快速移动";
}
else {
return "无效指令";
}
}
}
//距离解释:终结符表达式
class DistanceNode extends AbstractNode {
private String distance;
public DistanceNode(String distance) {
this.distance = distance;
}
//距离表达式的解释操作
public String interpret() {
return this.distance;
}
}
import java.util.Stack;
//指令处理类:工具类
class InstructionHandler {
private String instruction;
private AbstractNode node;
public void handle(String instruction) {
AbstractNode left = null, right = null;
AbstractNode direction = null, action = null, distance = null;
Stack stack = new Stack(); //声明一个栈对象用于存储抽象语法树
String[] words = instruction.split(" "); //以空格分隔指令字符串
for (int i = 0; i < words.length; i++) {
//本实例采用栈的方式来处理指令,如果遇到“and”,则将其后的三个单词作为三个终结符表达式连成一个简单句子SentenceNode作为“and”的右表达式,而将从栈顶弹出的表达式作为“and”的左表达式,
// 最后将新的“and”表达式压入栈中。
if (words[i].equalsIgnoreCase("and")) {
if (stack.isEmpty()) {
throw new IllegalArgumentException("Invalid instruction: cannot use 'and' at the beginning.");
}
left = (AbstractNode) stack.pop(); //弹出栈顶表达式作为左表达式
String word1 = words[++i];
direction = new DirectionNode(word1);
String word2 = words[++i];
action = new ActionNode(word2);
String word3 = words[++i];
distance = new DistanceNode(word3);
right = new SentenceNode(direction, action, distance); //右表达式
stack.push(new AndNode(left, right)); //将新表达式压入栈中
}
//如果是从头开始进行解释,则将前三个单词组成一个简单句子SentenceNode并将该句子压入栈中
else{
String word1 = words[i];
direction = new DirectionNode(word1);
String word2 = words[++i];
action = new ActionNode(word2);
String word3 = words[++i];
distance = new DistanceNode(word3);
left = new SentenceNode(direction, action, distance);
stack.push(left); //将新表达式压入栈中
}
}
this.node = (AbstractNode) stack.pop(); //将全部表达式从栈中弹出
}
public String output() {
String result = node.interpret(); //解释表达式
return result;
}
}
客户端代码
class Client {
public static void main(String args[]) {
String instruction = "up move 5 and down run 10 and left move 5";
InstructionHandler handler = new InstructionHandler();
handler.handle(instruction);
String outString;
outString = handler.output();
System.out.println(outString);
}
}
输出结果
向上移动5再向下快速移动10再向左移动5
在解释器模式中,环境类Context用于存储解释器之外的一些全局信息,它通常作为参数被传递到所有表达式的解释方法interpret()中,可以在Context对象中存储和访问表达式解释器的状态,向表达式解释器提供一些全局的、公共的数据,此外还可以在Context中增加一些所有表达式解释器都共有的功能,减轻解释器的职责。
在上面的机器人控制程序实例中,我们省略了环境类角色,下面再通过一个简单实例来说明环境类的用途:
Sunny软件公司开发了一套简单的基于字符界面的格式化指令,可以根据输入的指令在字符界面中输出一些格式化内容,例如输入
“LOOP 2 PRINT杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT郭靖 SPACE SPACE PRINT 黄蓉”,
将输出如下结果:
杨过 小龙女
杨过 小龙女
郭靖 黄蓉
其中关键词LOOP表示“循环”,后面的数字表示循环次数;PRINT表示“打印”,后面的字符串表示打印的内容;SPACE表示“空格”;BREAK表示“换行”;END表示“循环结束”。每一个关键词对应一条命令,计算机程序将根据关键词执行相应的处理操作。
现使用解释器模式设计并实现该格式化指令的解释,对指令进行分析并调用相应的操作执行指令中每一条命令。
Sunny软件公司开发人员通过分析,根据该格式化指令中句子的组成,定义了如下文法规则:
expression ::= command\* //表达式,一个表达式包含多条命令
command ::= loop | primitive //语句命令
loop ::= 'loop**number'** expression 'end' //循环命令,其中number为自然数
primitive ::= 'print**string'** | 'space' | 'break' //基本命令,其中string为字符串
根据以上文法规则,通过进一步分析,绘制如图结构

Context充当环境角色,Node充当抽象表达式角色,ExpressionNode、CommandNode和LoopCommandNode充当非终结符表达式角色,PrimitiveCommandNode充当终结符表达式角色。完整代码如下所示:
import java.util.*;
//环境类:用于存储和操作需要解释的语句,在本实例中每一个需要解释的单词可以称为一个动作标记(Action Token)或命令
class Context {
private StringTokenizer tokenizer; //StringTokenizer类,用于将字符串分解为更小的字符串标记(Token),默认情况下以空格作为分隔符
private String currentToken; //当前字符串标记
public Context(String text) {
tokenizer = new StringTokenizer(text); //通过传入的指令字符串创建StringTokenizer对象
nextToken();
}
//返回下一个标记
public String nextToken() {
if (tokenizer.hasMoreTokens()) {
currentToken = tokenizer.nextToken();
}
else {
currentToken = null;
}
return currentToken;
}
//返回当前的标记
public String currentToken() {
return currentToken;
}
//跳过一个标记
public void skipToken(String token) {
if (!token.equals(currentToken)) {
System.err.println("错误提示:" + currentToken + "解释错误!");
}
nextToken();
}
//如果当前的标记是一个数字,则返回对应的数值
public int currentNumber() {
int number = 0;
try{
number = Integer.parseInt(currentToken); //将字符串转换为整数
}
catch(NumberFormatException e) {
System.err.println("错误提示:" + e);
}
return number;
}
}
//抽象节点类:抽象表达式
abstract class Node {
public abstract void interpret(Context text); //声明一个方法用于解释语句
public abstract void execute(); //声明一个方法用于执行标记对应的命令
}
//表达式节点类:非终结符表达式
class ExpressionNode extends Node {
private ArrayList<Node> list = new ArrayList<Node>(); //定义一个集合用于存储多条命令
public void interpret(Context context) {
//循环处理Context中的标记
while (true){
//如果已经没有任何标记,则退出解释
if (context.currentToken() == null) {
break;
}
//如果标记为END,则不解释END并结束本次解释过程,可以继续之后的解释
else if (context.currentToken().equals("END")) {
context.skipToken("END");
break;
}
//如果为其他标记,则解释标记并将其加入命令集合
else {
Node commandNode = new CommandNode();
commandNode.interpret(context);
list.add(commandNode);
}
}
}
//循环执行命令集合中的每一条命令
public void execute() {
Iterator iterator = list.iterator();
while (iterator.hasNext()){
((Node)iterator.next()).execute();
}
}
}
//语句命令节点类:非终结符表达式
class CommandNode extends Node {
private Node node;
public void interpret(Context context) {
//处理LOOP循环命令
if (context.currentToken().equals("LOOP")) {
node = new LoopCommandNode();
node.interpret(context);
}
//处理其他基本命令
else {
node = new PrimitiveCommandNode();
node.interpret(context);
}
}
public void execute() {
node.execute();
}
}
//循环命令节点类:非终结符表达式
class LoopCommandNode extends Node {
private int number; //循环次数
private Node commandNode; //循环语句中的表达式
//解释循环命令
public void interpret(Context context) {
context.skipToken("LOOP");
number = context.currentNumber();
context.nextToken();
commandNode = new ExpressionNode(); //循环语句中的表达式
commandNode.interpret(context);
}
public void execute() {
for (int i=0;i<number;i++)
commandNode.execute();
}
}
//基本命令节点类:终结符表达式
class PrimitiveCommandNode extends Node {
private String name;
private String text;
//解释基本命令
public void interpret(Context context) {
name = context.currentToken();
context.skipToken(name);
if (!name.equals("PRINT") && !name.equals("BREAK") && !name.equals ("SPACE")){
System.err.println("非法命令!");
}
if (name.equals("PRINT")){
text = context.currentToken();
context.nextToken();
}
}
public void execute(){
if (name.equals("PRINT"))
System.out.print(text);
else if (name.equals("SPACE"))
System.out.print(" ");
else if (name.equals("BREAK"))
System.out.println();
}
}
在本实例代码中,环境类Context类似一个工具类,它提供了用于处理指令的方法,如nextToken()、currentToken()、skipToken()等,同时它存储了需要解释的指令并记录了每一次解释的当前标记(Token),而具体的解释过程交给表达式解释器类来处理。我们还可以将各种解释器类包含的公共方法移至环境类中,更好地实现这些方法的重用和扩展。
客户端代码
class Client{
public static void main(String[] args){
String text = "LOOP 2 PRINT 杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黄蓉";
Context context = new Context(text);
Node node = new ExpressionNode();
node.interpret(context);
node.execute();
}
}
解释器模式为自定义语言的设计和实现提供了一种解决方案,它用于定义一组文法规则并通过这组文法规则来解释语言中的句子。
虽然解释器模式的使用频率不是特别高,但是它在正则表达式、XML文档解释等领域还是得到了广泛使用。
与解释器模式类似,目前还诞生了很多基于抽象语法树的源代码处理工具,例如Eclipse中的Eclipse AST,它可以用于表示Java语言的语法结构,用户可以通过扩展其功能,创建自己的文法规则。
###优点
解释器模式的主要优点如下:
(1) 易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
(2) 每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
(3) 实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码。
(4) 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合“开闭原则”。
解释器模式的主要缺点如下:
(1) 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式。
(2) 执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。
在以下情况下可以考虑使用解释器模式:
(1) 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
(2) 一些重复出现的问题可以用一种简单的语言来进行表达。
(3) 一个语言的文法较为简单。
(4) 执行效率不是关键问题。【注:高效的解释器通常不是通过直接解释抽象语法树来实现的,而是需要将它们转换成其他形式,使用解释器模式的执行效率并不高。】
Sunny软件公司欲为数据库备份和同步开发一套简单的数据库同步指令,通过指令可以对数据库中的数据和结构进行备份,例如,输入指令“COPY VIEW FROM srcDB TO desDB”表示将数据库srcDB中的所有视图(View)对象都拷贝至数据库desDB;输入指令“MOVE TABLE Student FROM srcDB TO desDB”表示将数据库srcDB中的Student表移动至数据库desDB。试使用解释器模式来设计并实现该数据库同步指令。
解释器模式的难度相对较大, 其实重点就是组合组织语法树和解析语法树。理解解释器模式对理解编程语言编译器的实现有很大的帮助
在软件开发中,我们经常需要使用聚合对象来存储一系列数据。
聚合对象拥有两个职责:一是存储数据;二是遍历数据。
从依赖性来看,前者是聚合对象的基本职责;而后者既是可变化的,又是可分离的。
因此,可以将遍历数据的行为从聚合对象中分离出来,封装在一个被称之为“迭代器”的对象中,由迭代器来提供遍历聚合对象内部数据的行为,这将简化聚合对象的设计,更符合“单一职责原则”的要求。
迭代器模式(Iterator Pattern):提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor)。迭代器模式是一种对象行为型模式。
在迭代器模式结构中包含聚合和迭代器两个层次结构,考虑到系统的灵活性和可扩展性,在迭代器模式中应用了工厂方法模式

**● Iterator(抽象迭代器):**它定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法,例如:用于获取第一个元素的first()方法,用于访问下一个元素的next()方法,用于判断是否还有下一个元素的hasNext()方法,用于获取当前元素的currentItem()方法等,在具体迭代器中将实现这些方法。
**● ConcreteIterator(具体迭代器):**它实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时,游标通常是一个表示位置的非负整数。
**● Aggregate(抽象聚合类):**它用于存储和管理元素对象,声明一个createIterator()方法用于创建一个迭代器对象,充当抽象迭代器工厂角色。
**● ConcreteAggregate(具体聚合类):**它实现了在抽象聚合类中声明的createIterator()方法,该方法返回一个与该具体聚合类对应的具体迭代器ConcreteIterator实例。
在迭代器模式中,提供了一个外部的迭代器来对聚合对象进行访问和遍历,迭代器定义了一个访问该聚合元素的接口,并且可以跟踪当前遍历的元素,了解哪些元素已经遍历过而哪些没有。
迭代器的引入,将使得对一个复杂聚合对象的操作变得简单。
下面我们结合代码来对迭代器模式的结构进行进一步分析。在迭代器模式中应用了工厂方法模式,抽象迭代器对应于抽象产品角色,具体迭代器对应于具体产品角色,抽象聚合类对应于抽象工厂角色,具体聚合类对应于具体工厂角色。
在抽象迭代器中声明了用于遍历聚合对象中所存储元素的方法,典型代码如下所示:
interface Iterator {
public void first(); //将游标指向第一个元素
public void next(); //将游标指向下一个元素
public boolean hasNext(); //判断是否存在下一个元素
public Object currentItem(); //获取游标指向的当前元素
}
在具体迭代器中将实现抽象迭代器声明的遍历数据的方法,如下代码所示:
class ConcreteIterator implements Iterator {
private ConcreteAggregate objects; //维持一个对具体聚合对象的引用,以便于访问存储在聚合对象中的数据
private int cursor; //定义一个游标,用于记录当前访问位置
public ConcreteIterator(ConcreteAggregate objects) {
this.objects=objects;
}
public void first() { ...... }
public void next() { ...... }
public boolean hasNext() { ...... }
public Object currentItem() { ...... }
}
需要注意的是抽象迭代器接口的设计非常重要,
一方面需要充分满足各种遍历操作的要求,尽量为各种遍历方法都提供声明,
另一方面又不能包含太多方法,接口中方法太多将给子类的实现带来麻烦。
因此,可以考虑使用抽象类来设计抽象迭代器,在抽象类中为每一个方法提供一个空的默认实现。
如果需要在具体迭代器中为聚合对象增加全新的遍历操作,则必须修改抽象迭代器和具体迭代器的源代码,这将违反“开闭原则”,因此在设计时要考虑全面,避免之后修改接口。
聚合类用于存储数据并负责创建迭代器对象,最简单的抽象聚合类代码如下所示:
interface Aggregate {
Iterator createIterator();
}
具体聚合类作为抽象聚合类的子类,一方面负责存储数据,另一方面实现了在抽象聚合类中声明的工厂方法createIterator(),用于返回一个与该具体聚合类对应的具体迭代器对象.
代码如下所示:
class ConcreteAggregate implements Aggregate {
......
public Iterator createIterator() {
return new ConcreteIterator(this);
}
......
}
Sunny软件公司为某商场开发了一套销售管理系统,在对该系统进行分析和设计时,Sunny软件公司开发人员发现经常需要对系统中的商品数据、客户数据等进行遍历,
为了复用这些遍历代码,Sunny公司开发人员设计了一个抽象的数据集合类AbstractObjectList,而将存储商品和客户等数据的类作为其子类,AbstractObjectList类结构如图所示:

List类型的对象objects用于存储数据,方法说明如表1所示:
表1 AbstractObjectList类方法说明
| 方法名 | 方法说明 |
|---|---|
| AbstractObjectList() | 构造方法,用于给objects对象赋值 |
| addObject() | 增加元素 |
| removeObject() | 删除元素 |
| getObjects() | 获取所有元素 |
| next() | 移至下一个元素 |
| isLast() | 判断当前元素是否是最后一个元素 |
| previous() | 移至上一个元素 |
| isFirst() | 判断当前元素是否是第一个元素 |
| getNextItem() | 获取下一个元素 |
| getPreviousItem() | 获取上一个元素 |
AbstractObjectList类的子类ProductList和CustomerList分别用于存储商品数据和客户数据。
Sunny软件公司开发人员通过对AbstractObjectList类结构进行分析,发现该设计方案存在如下几个问题:
(1) 在图中,addObject()、removeObject()等方法用于管理数据,而next()、isLast()、previous()、isFirst()等方法用于遍历数据。这将导致聚合类的职责过重,它既负责存储和管理数据,又负责遍历数据,违反了“单一职责原则”,由于聚合类非常庞大,实现代码过长,还将给测试和维护增加难度。
(2) 如果将抽象聚合类声明为一个接口,则在这个接口中充斥着大量方法,不利于子类实现,违反了“接口隔离原则”。
(3) 如果将所有的遍历操作都交给子类来实现,将导致子类代码庞大,而且必须暴露AbstractObjectList的内部存储细节,向子类公开自己的私有属性,否则子类无法实施对数据的遍历,这将破坏AbstractObjectList类的封装性。
如何解决上述问题?解决方案之一就是将聚合类中负责遍历数据的方法提取出来,封装到专门的类中,实现数据存储和数据遍历分离,无须暴露聚合类的内部属性即可对其进行操作
为了简化AbstractObjectList类的结构,并给不同的具体数据集合类提供不同的遍历方式,Sunny软件公司开发人员使用迭代器模式来重构AbstractObjectList类的设计,重构之后的销售管理系统数据遍历结构如图:

(注:为了简化类图和代码,本结构图中只提供一个具体聚合类和具体迭代器类)
AbstractObjectList充当抽象聚合类,ProductList充当具体聚合类,AbstractIterator充当抽象迭代器,ProductIterator充当具体迭代器。完整代码如下所示:
//在本实例中,为了详细说明自定义迭代器的实现过程,我们没有使用JDK中内置的迭代器,事实上,JDK内置迭代器已经实现了对一个List对象的正向遍历
import java.util.*;
//抽象聚合类
abstract class AbstractObjectList {
protected List<Object> objects = new ArrayList<Object>();
public AbstractObjectList(List objects) {
this.objects = objects;
}
public void addObject(Object obj) {
this.objects.add(obj);
}
public void removeObject(Object obj) {
this.objects.remove(obj);
}
public List getObjects() {
return this.objects;
}
//声明创建迭代器对象的抽象工厂方法
public abstract AbstractIterator createIterator();
}
//商品数据类:具体聚合类
class ProductList extends AbstractObjectList {
public ProductList(List products) {
super(products);
}
//实现创建迭代器对象的具体工厂方法
public AbstractIterator createIterator() {
return new ProductIterator(this);
}
}
//抽象迭代器
interface AbstractIterator {
public void next(); //移至下一个元素
public boolean isLast(); //判断是否为最后一个元素
public void previous(); //移至上一个元素
public boolean isFirst(); //判断是否为第一个元素
public Object getNextItem(); //获取下一个元素
public Object getPreviousItem(); //获取上一个元素
}
//商品迭代器:具体迭代器
class ProductIterator implements AbstractIterator {
private ProductList productList;
private List products;
private int cursor1; //定义一个游标,用于记录正向遍历的位置
private int cursor2; //定义一个游标,用于记录逆向遍历的位置
public ProductIterator(ProductList list) {
this.productList = list;
this.products = list.getObjects(); //获取集合对象
cursor1 = 0; //设置正向遍历游标的初始值
cursor2 = products.size() -1; //设置逆向遍历游标的初始值
}
public void next() {
if(cursor1 < products.size()) {
cursor1++;
}
}
public boolean isLast() {
return (cursor1 == products.size());
}
public void previous() {
if (cursor2 > -1) {
cursor2--;
}
}
public boolean isFirst() {
return (cursor2 == -1);
}
public Object getNextItem() {
return products.get(cursor1);
}
public Object getPreviousItem() {
return products.get(cursor2);
}
}
编写如下客户端测试代码:
class Client {
public static void main(String args[]) {
List products = new ArrayList();
products.add("倚天剑");
products.add("屠龙刀");
products.add("断肠草");
products.add("葵花宝典");
products.add("四十二章经");
AbstractObjectList list;
AbstractIterator iterator;
list = new ProductList(products); //创建聚合对象
iterator = list.createIterator(); //创建迭代器对象
System.out.println("正向遍历:");
while(!iterator.isLast()) {
System.out.print(iterator.getNextItem() + ",");
iterator.next();
}
System.out.println();
System.out.println("-----------------------------");
System.out.println("逆向遍历:");
while(!iterator.isFirst()) {
System.out.print(iterator.getPreviousItem() + ",");
iterator.previous();
}
}
}
编译并运行程序,输出结果如下:
正向遍历:
倚天剑,屠龙刀,断肠草,葵花宝典,四十二章经,
-----------------------------
逆向遍历:
四十二章经,葵花宝典,断肠草,屠龙刀,倚天剑,
如果需要增加一个新的具体聚合类,如客户数据集合类,并且需要为客户数据集合类提供不同于商品数据集合类的正向遍历和逆向遍历操作,只需增加一个新的聚合子类和一个新的具体迭代器类即可,原有类库代码无须修改,符合“开闭原则”;
如果需要为ProductList类更换一个迭代器,只需要增加一个新的具体迭代器类作为抽象迭代器类的子类,重新实现遍历方法,原有迭代器代码无须修改,也符合“开闭原则”;但是如果要在迭代器中增加新的方法,则需要修改抽象迭代器源代码,这将违背“开闭原则”。
在迭代器模式结构图中,我们可以看到具体迭代器类和具体聚合类之间存在双重关系,其中一个关系为关联关系,在具体迭代器中需要维持一个对具体聚合对象的引用,该关联关系的目的是访问存储在聚合对象中的数据,以便迭代器能够对这些数据进行遍历操作。
除了使用关联关系外,为了能够让迭代器可以访问到聚合对象中的数据,我们还可以将迭代器类设计为聚合类的内部类,JDK中的迭代器类就是通过这种方法来实现的,如下AbstractList类代码片段所示:
package java.util;
……
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
......
private class Itr implements Iterator<E> {
int cursor = 0;
......
}
……
}
我们可以通过类似的方法来设计第3节中的ProductList类,将ProductIterator类作为ProductList类的内部类,代码如下所示:
//商品数据类:具体聚合类
class ProductList extends AbstractObjectList {
public ProductList(List products) {
super(products);
}
public AbstractIterator createIterator() {
return new ProductIterator();
}
//商品迭代器:具体迭代器,内部类实现
private class ProductIterator implements AbstractIterator {
private int cursor1;
private int cursor2;
public ProductIterator() {
cursor1 = 0;
cursor2 = objects.size() -1;
}
public void next() {
if(cursor1 < objects.size()) {
cursor1++;
}
}
public boolean isLast() {
return (cursor1 == objects.size());
}
public void previous() {
if(cursor2 > -1) {
cursor2--;
}
}
public boolean isFirst() {
return (cursor2 == -1);
}
public Object getNextItem() {
return objects.get(cursor1);
}
public Object getPreviousItem() {
return objects.get(cursor2);
}
}
}
删除原来的 ProductIterator
无论使用哪种实现机制,客户端代码都是一样的,也就是说客户端无须关心具体迭代器对象的创建细节,只需通过调用工厂方法createIterator()即可得到一个可用的迭代器对象,这也是使用工厂方法模式的好处,通过工厂来封装对象的创建过程,简化了客户端的调用。
为了让开发人员能够更加方便地操作聚合对象,在Java、C#等编程语言中都提供了内置迭代器。
在Java集合框架中,常用的List和Set等聚合类都继承(或实现)了java.util.Collection接口,在Collection接口中声明了如下方法(部分):
package java.util;
public interface Collection<E> extends Iterable<E> {
……
boolean add(Object c);
boolean addAll(Collection c);
boolean remove(Object o);
boolean removeAll(Collection c);
boolean remainAll(Collection c);
Iterator iterator();
……
}
除了包含一些增加元素和删除元素的方法外,还提供了一个iterator()方法,用于返回一个Iterator迭代器对象,以便遍历聚合中的元素;
具体的Java聚合类可以通过实现该iterator()方法返回一个具体的Iterator对象。
JDK中定义了抽象迭代器接口Iterator,代码如下所示:
package java.util;
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
其中,hasNext()用于判断聚合对象中是否还存在下一个元素,为了不抛出异常,在每次调用next()之前需先调用hasNext(),如果有可供访问的元素,则返回true;
next()方法用于将游标移至下一个元素,通过它可以逐个访问聚合中的元素,它返回游标所越过的那个元素的引用;
remove()方法用于删除上次调用next()时所返回的元素。
Java迭代器工作原理如下图所示,在第一个next()方法被调用时,迭代器游标由“元素1”与“元素2”之间移至“元素2”与“元素3”之间,跨越了“元素2”,因此next()方法将返回对“元素2”的引用;
在第二个next()方法被调用时,迭代器由“元素2”与“元素3”之间移至“元素3”和“元素4”之间,next()方法将返回对“元素3”的引用,如果此时调用remove()方法,即可将“元素3”删除。

如下代码片段可用于删除聚合对象中的第一个元素:
Iterator iterator = collection.iterator(); //collection是已实例化的聚合对象
iterator.next(); // 跳过第一个元素
iterator.remove(); // 删除第一个元素
需要注意的是,在这里,next()方法与remove()方法的调用是相互关联的。
如果调用remove()之前,没有先对next()进行调用,那么将会抛出一个IllegalStateException异常,因为没有任何可供删除的元素。
如下代码片段可用于删除两个相邻的元素:
iterator.remove();
iterator.next(); //如果删除此行代码程序将抛异常
iterator.remove();
在上面的代码片段中如果将代码iterator.next();去掉则程序运行抛异常,因为第二次删除时将找不到可供删除的元素。
在JDK中,Collection接口和Iterator接口充当了迭代器模式的抽象层,分别对应于抽象聚合类和抽象迭代器,而Collection接口的子类充当了具体聚合类,下面以List为例加以说明,下图列出了JDK中部分与List有关的类及它们之间的关系:

(注:为了简化类图,本图省略了大量方法)
在JDK中,实际情况比上图要复杂很多,在图中,List接口除了继承Collection接口的iterator()方法外,还增加了新的工厂方法listIterator(),专门用于创建ListIterator类型的迭代器,在List的子类LinkedList中实现了该方法,可用于创建具体的ListIterator子类ListItr的对象,代码如下所示:
public ListIterator<E> listIterator(int index) {
return new ListItr(index);
}
listIterator()方法用于返回具体迭代器ListItr类型的对象。在JDK源码中,AbstractList中的iterator()方法调用了listIterator()方法,如下代码所示:
public Iterator<E> iterator() {
return listIterator();
}
客户端通过调用LinkedList类的iterator()方法,即可得到一个专门用于遍历LinkedList的迭代器对象。
大家可能会问?既然有了iterator()方法,为什么还要提供一个listIterator()方法呢?这两个方法的功能不会存在重复吗?干嘛要多此一举?
这是一个好问题。我给大家简单解释一下为什么要这样设计:由于在Iterator接口中定义的方法太少,只有三个,通过这三个方法只能实现正向遍历,而有时候我们需要对一个聚合对象进行逆向遍历等操作,因此在JDK的ListIterator接口中声明了用于逆向遍历的hasPrevious()和previous()等方法,如果客户端需要调用这两个方法来实现逆向遍历,就不能再使用iterator()方法来创建迭代器了,因为此时创建的迭代器对象是不具有这两个方法的。我们只能通过如下代码来创建ListIterator类型的迭代器对象:
ListIterator i = c.listIterator();
正因为如此,在JDK的List接口中不得不增加对listIterator()方法的声明,该方法可以返回一个ListIterator类型的迭代器,ListIterator迭代器具有更加强大的功能。
思考:为什么使用iterator()方法创建的迭代器无法实现逆向遍历?
在Java语言中,我们可以直接使用JDK内置的迭代器来遍历聚合对象中的元素,下面的代码演示了如何使用Java内置的迭代器:
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
//如何使用Java内置的迭代器
public class Client {
public static void process(Collection c) {
Iterator i = c.iterator(); //创建迭代器对象
//通过迭代器遍历聚合对象
while(i.hasNext()) {
System.out.println(i.next().toString());
}
}
public static void main(String[] args) {
Collection persons;
persons = new ArrayList(); //创建一个ArrayList类型的聚合对象
persons.add("张无忌");
persons.add("小龙女");
persons.add("令狐冲");
persons.add("韦小宝");
persons.add("袁紫衣");
persons.add("小龙女");
System.out.printf("ArrayList输出");
process(persons);
System.out.printf("-------------------------------------------");
persons = new HashSet(); //创建一个ArrayList类型的聚合对象
persons.add("张无忌");
persons.add("小龙女");
persons.add("令狐冲");
persons.add("韦小宝");
persons.add("袁紫衣");
persons.add("小龙女");
System.out.printf("HashSet输出");
process(persons);
}
}
在静态方法process()中使用迭代器Iterator对Collection对象进行处理,该代码运行结果如下:
ArrayList输出张无忌
小龙女
令狐冲
韦小宝
袁紫衣
小龙女
-------------------------------------------HashSet输出令狐冲
韦小宝
小龙女
袁紫衣
张无忌
在HashSet中合并了重复元素,并且元素以随机次序输出,其结果与使用ArrayList不相同。
由此可见,通过使用迭代器模式,使得更换具体聚合类变得非常方便,而且还可以根据需要增加新的聚合类,新的聚合类只需要实现Collection接口,无须修改原有类库代码,符合“开闭原则”。
迭代器模式是一种使用频率非常高的设计模式,通过引入迭代器可以将数据的遍历功能从聚合对象中分离出来,聚合对象只负责存储数据,而遍历数据由迭代器来完成。由于很多编程语言的类库都已经实现了迭代器模式,因此在实际开发中,我们只需要直接使用Java、C#等语言已定义好的迭代器即可,迭代器已经成为我们操作聚合对象的基本工具之一。
(1) 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。
(2) 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。
(3) 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足“开闭原则”的要求。
(1) 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
(2) 抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展,例如JDK内置迭代器Iterator就无法实现逆向遍历,如果需要实现逆向遍历,只能通过其子类ListIterator等来实现,而ListIterator迭代器无法用于操作Set类型的聚合对象。在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是件很容易的事情。
(1) 访问一个聚合对象的内容而无须暴露它的内部表示。将聚合对象的访问与内部数据的存储分离,使得访问聚合对象时无须了解其内部实现细节。
(2) 需要为一个聚合对象提供多种遍历方式。
(3) 为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口。
如果在一个系统中对象之间的联系呈现为网状结构,如图1所示。
对象之间存在大量的多对多联系,将导致系统非常复杂,这些对象既会影响别的对象,也会被别的对象所影响,这些对象称为同事对象,它们之间通过彼此的相互作用实现系统的行为。
在网状结构中,几乎每个对象都需要与其他对象发生相互作用,而这种相互作用表现为一个对象与另外一个对象的直接耦合,这将导致一个过度耦合的系统。

图1 对象之间存在复杂关系的网状结构
中介者模式可以使对象之间的关系数量急剧减少,通过引入中介者对象,可以将系统的网状结构变成以中介者为中心的星形结构,如图2所示。
在这个星形结构中,同事对象不再直接与另一个对象联系,它通过中介者对象与另一个对象发生相互作用。中介者对象的存在保证了对象结构上的稳定,也就是说,系统的结构不会因为新对象的引入带来大量的修改工作。

图2 引入中介者对象的星型结构
如果在一个系统中对象之间存在多对多的相互关系,我们可以将对象之间的一些交互行为从各个对象中分离出来,并集中封装在一个中介者对象中,并由该中介者进行统一协调,这样对象之间多对多的复杂关系就转化为相对简单的一对多关系。
通过引入中介者来简化对象之间的复杂交互,中介者模式是“迪米特法则”的一个典型应用。
中介者模式(Mediator Pattern):用一个中介对象(中介者)来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式。
在中介者模式中,我们引入了用于协调其他对象/类之间相互调用的中介者类,为了让系统具有更好的灵活性和可扩展性,通常还提供了抽象中介者,其结构图如图3所示:

图3 中介者模式结构图
● Mediator**(抽象中介者):**它定义一个接口,该接口用于与各同事对象之间进行通信。
● ConcreteMediator**(具体中介者):**它是抽象中介者的子类,通过协调各个同事对象来实现协作行为,它维持了对各个同事对象的引用。
● Colleague**(抽象同事类):**它定义各个同事类公有的方法,并声明了一些抽象方法来供子类实现,同时它维持了一个对抽象中介者类的引用,其子类可以通过该引用来与中介者通信。
● ConcreteColleague**(具体同事类):**它是抽象同事类的子类;每一个同事对象在需要和其他同事对象通信时,先与中介者通信,通过中介者来间接完成与其他同事类的通信;在具体同事类中实现了在抽象同事类中声明的抽象方法。
中介者模式的核心在于中介者类的引入,在中介者模式中,中介者类承担了两方面的职责:
(1) **中转作用(结构性):**通过中介者提供的中转作用,各个同事对象就不再需要显式引用其他同事,当需要和其他同事进行通信时,可通过中介者来实现间接调用。该中转作用属于中介者在结构上的支持。
(2) **协调作用(行为性):**中介者可以更进一步的对同事之间的关系进行封装,同事可以一致的和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装。该协调作用属于中介者在行为上的支持。
在中介者模式中,典型的抽象中介者类代码如下所示:
abstract class Mediator {
protected ArrayList<Colleague> colleagues; //用于存储同事对象
//注册方法,用于增加同事对象
public void register(Colleague colleague) {
colleagues.add(colleague);
}
//声明抽象的业务方法
public abstract void operation();
}
在抽象中介者中可以定义一个同事类的集合,用于存储同事对象并提供注册方法,同时声明了具体中介者类所具有的方法。在具体中介者类中将实现这些抽象方法,典型的具体中介者类代码如下所示:
class ConcreteMediator extends Mediator {
//实现业务方法,封装同事之间的调用
public void operation() {
......
((Colleague)(colleagues.get(0))).method1(); //通过中介者调用同事类的方法
......
}
}
在具体中介者类中将调用同事类的方法,调用时可以增加一些自己的业务代码对调用进行控制。
在抽象同事类中维持了一个抽象中介者的引用,用于调用中介者的方法,典型的抽象同事类代码如下所示:
abstract class Colleague {
protected Mediator mediator; //维持一个抽象中介者的引用
public Colleague(Mediator mediator) {
this.mediator=mediator;
}
public abstract void method1(); //声明自身方法,处理自己的行为
//定义依赖方法,与中介者进行通信
public void method2() {
mediator.operation();
}
}
在抽象同事类中声明了同事类的抽象方法,而在具体同事类中将实现这些方法,典型的具体同事类代码如下所示:
class ConcreteColleague extends Colleague {
public ConcreteColleague(Mediator mediator) {
super(mediator);
}
//实现自身方法
public void method1() {
......
}
}
在具体同事类ConcreteColleague中实现了在抽象同事类中声明的方法,其中方法method1()是同事类的自身方法(Self-Method),用于处理自己的行为,而方法method2()是依赖方法(Depend-Method),用于调用在中介者中定义的方法,依赖中介者来完成相应的行为,例如调用另一个同事类的相关方法。
Sunny软件公司欲开发一套CRM系统,其中包含一个客户信息管理模块,所设计的“客户信息管理窗口”界面效果图如图所示:

图20-2 “客户信息管理窗口”界面图
Sunny公司开发人员通过分析发现,在上图中,界面组件之间存在较为复杂的交互关系:如果删除一个客户,要在客户列表(List)中删掉对应的项,客户选择组合框(ComboBox)中客户名称也将减少一个;如果增加一个客户信息,客户列表中需增加一个客户,且组合框中也将增加一项。
如果实现界面组件之间的交互是Sunny公司开发人员必须面对的一个问题?
Sunny公司开发人员对组件之间的交互关系进行了分析,结果如下:
(1) 当用户单击“增加”按钮、“删除”按钮、“修改”按钮或“查询”按钮时,界面左侧的“客户选择组合框”、“客户列表”以及界面中的文本框将产生响应。
(2) 当用户通过“客户选择组合框”选中某个客户姓名时,“客户列表”和文本框将产生响应。
(3) 当用户通过“客户列表”选中某个客户姓名时,“客户选择组合框”和文本框将产生响应。
于是,Sunny公司开发人员根据组件之间的交互关系绘制了如图所示初始类图:

与类图对应的框架代码片段如下:
//按钮类
class Button {
private List list;
private ComboBox cb;
private TextBox tb;
......
//界面组件的交互
public void change() {
list.update();
cb.update();
tb.update();
}
public void update() {
......
}
......
}
//列表框类
class List {
private ComboBox cb;
private TextBox tb;
......
//界面组件的交互
public void change() {
cb.update();
tb.update();
}
public void update() {
......
}
......
}
//组合框类
class ComboBox {
private List list;
private TextBox tb;
......
//界面组件的交互
public void change() {
list.update();
tb.update();
}
public void update() {
......
}
......
}
//文本框类
class TextBox {
public void update() {
......
}
......
}
分析类图初始结构图和上述代码,我们不难发现该设计方案存在如下问题:
(1) 系统结构复杂且耦合度高:每一个界面组件都与多个其他组件之间产生相互关联和调用,若一个界面组件对象发生变化,需要跟踪与之有关联的其他所有组件并进行处理,系统组件之间呈现一种较为复杂的网状结构,组件之间的耦合度高。
(2) 组件的可重用性差:由于每一个组件和其他组件之间都具有很强的关联,若没有其他组件的支持,一个组件很难被另一个系统或模块重用,这些组件表现出来更像一个不可分割的整体,而在实际使用时,我们往往需要每一个组件都能够单独重用,而不是重用一个由多个组件组成的复杂结构。
(3) 系统的可扩展性差:如果在上述系统中增加一个新的组件类,则必须修改与之交互的其他组件类的源代码,将导致多个类的源代码需要修改,同样,如果要删除一个组件也存在类似的问题,这违反了“开闭原则”,可扩展性和灵活性欠佳。
由于存在上述问题,Sunny公司开发人员不得不对原有系统进行重构,那如何重构呢?大家想到了“迪米特法则”,引入一个“第三者”来降低现有系统中类之间的耦合度。由这个“第三者”来封装并协调原有组件两两之间复杂的引用关系,使之成为一个松耦合的系统,这个“第三者”又称为“中介者”,中介者模式因此而得名。下面让我们正式进入中介者模式的学习,学会如何使用中介者类来协调多个类/对象之间的交互,以达到降低系统耦合度的目的。
为了协调界面组件对象之间的复杂交互关系,Sunny公司开发人员使用中介者模式来设计客户信息管理窗口,其结构示意图如图所示:

图引入了中介者类的“客户信息管理窗口”结构示意图
上图只是一个重构之后的结构示意图,在具体实现时,为了确保系统具有更好的灵活性和可扩展性,我们需要定义抽象中介者和抽象组件类,其中抽象组件类是所有具体组件类的公共父类,完整类图如下所示:

图 重构后的“客户信息管理窗口”结构图
在上图中,Component充当抽象同事类,Button、List、ComboBox和TextBox充当具体同事类,Mediator充当抽象中介者类,ConcreteMediator充当具体中介者类,ConcreteMediator维持了对具体同事类的引用,为了简化ConcreteMediator类的代码,我们在其中只定义了一个Button对象和一个TextBox对象。完整代码如下所示:
//抽象中介者
abstract class Mediator {
public abstract void componentChanged(Component c);
}
//具体中介者
class ConcreteMediator extends Mediator {
//维持对各个同事对象的引用
public Button addButton;
public List list;
public TextBox userNameTextBox;
public ComboBox cb;
//封装同事对象之间的交互
public void componentChanged(Component c) {
//单击按钮
if(c == addButton) {
System.out.println("--单击增加按钮--");
list.update();
cb.update();
userNameTextBox.update();
}
//从列表框选择客户
else if(c == list) {
System.out.println("--从列表框选择客户--");
cb.select();
userNameTextBox.setText();
}
//从组合框选择客户
else if(c == cb) {
System.out.println("--从组合框选择客户--");
cb.select();
userNameTextBox.setText();
}
}
}
//抽象组件类:抽象同事类
abstract class Component {
protected Mediator mediator;
public void setMediator(Mediator mediator) {
this.mediator = mediator;
}
//转发调用
public void changed() {
mediator.componentChanged(this);
}
public abstract void update();
}
//按钮类:具体同事类
class Button extends Component {
public void update() {
//按钮不产生交互
}
}
//列表框类:具体同事类
class List extends Component {
public void update() {
System.out.println("列表框增加一项:张无忌。");
}
public void select() {
System.out.println("列表框选中项:小龙女。");
}
}
//组合框类:具体同事类
class ComboBox extends Component {
public void update() {
System.out.println("组合框增加一项:张无忌。");
}
public void select() {
System.out.println("组合框选中项:小龙女。");
}
}
//文本框类:具体同事类
class TextBox extends Component {
public void update() {
System.out.println("客户信息增加成功后文本框清空。");
}
public void setText() {
System.out.println("文本框显示:小龙女。");
}
}
编写如下客户端测试代码:
class Client {
public static void main(String args[]) {
//定义中介者对象
ConcreteMediator mediator;
mediator = new ConcreteMediator();
//定义同事对象
Button addBT = new Button();
List list = new List();
ComboBox cb = new ComboBox();
TextBox userNameTB = new TextBox();
addBT.setMediator(mediator);
list.setMediator(mediator);
cb.setMediator(mediator);
userNameTB.setMediator(mediator);
mediator.addButton = addBT;
mediator.list = list;
mediator.cb = cb;
mediator.userNameTextBox = userNameTB;
addBT.changed();
System.out.println("-----------------------------");
list.changed();
}
}
编译并运行程序,输出结果如下:
--单击增加按钮--
列表框增加一项:张无忌。
组合框增加一项:张无忌。
客户信息增加成功后文本框清空。
-----------------------------
--从列表框选择客户--
组合框选中项:小龙女。
文本框显示:小龙女。
Sunny软件公司CRM系统的客户对“客户信息管理窗口”提出了一个修改意见:要求在窗口的下端能够及时显示当前系统中客户信息的总数。修改之后的界面如下图所示:

修改之后的“客户信息管理窗口”界面图
从图中我们不难发现,可以通过增加一个**文本标签(Label)**来显示客户信息总数,而且当用户点击“增加”按钮或者“删除”按钮时,将改变文本标签的内容。
由于使用了中介者模式,在原有系统中增加新的组件(即新的同事类)将变得很容易,我们至少有如下两种解决方案:
**【解决方案一】**增加一个界面组件类Label,修改原有的具体中介者类ConcreteMediator,增加一个对Label对象的引用,然后修改componentChanged()方法中其他相关组件对象的业务处理代码,原有组件类无须任何修改,客户端代码也需针对新增组件Label进行适当修改。
**【解决方案二】**与方案一相同,首先增加一个Label类,但不修改原有具体中介者类ConcreteMediator的代码,而是增加一个ConcreteMediator的子类SubConcreteMediator来实现对Label对象的引用,然后在新增的中介者类SubConcreteMediator中通过覆盖componentChanged()方法来实现所有组件(包括新增Label组件)之间的交互,同样,原有组件类无须做任何修改,客户端代码需少许修改。
引入Label之后“客户信息管理窗口”类结构示意图如下图所示:

增加Label组件类后的“客户信息管理窗口”结构示意图
由于**【解决方案二】**无须修改ConcreteMediator类,更符合“开闭原则”,因此我们选择该解决方案来对新增Label类进行处理,对应的完整类图如下图所示:

修改之后的“客户信息管理窗口”结构图
在上图中,新增了具体同事类Label和具体中介者类SubConcreteMediator,代码如下所示:
//文本标签类:具体同事类
class Label extends Component {
public void update() {
System.out.println("文本标签内容改变,客户信息总数加1。");
}
}
//新增具体中介者类
class SubConcreteMediator extends ConcreteMediator {
//增加对Label对象的引用
public Label label;
public void componentChanged(Component c) {
//单击按钮
if(c == addButton) {
System.out.println("--单击增加按钮--");
list.update();
cb.update();
userNameTextBox.update();
label.update(); //文本标签更新
}
//从列表框选择客户
else if(c == list) {
System.out.println("--从列表框选择客户--");
cb.select();
userNameTextBox.setText();
}
//从组合框选择客户
else if(c == cb) {
System.out.println("--从组合框选择客户--");
cb.select();
userNameTextBox.setText();
}
}
}
修改客户端测试代码:
class Client {
public static void main(String args[]) {
//用新增具体中介者定义中介者对象
SubConcreteMediator mediator;
mediator = new SubConcreteMediator();
Button addBT = new Button();
List list = new List();
ComboBox cb = new ComboBox();
TextBox userNameTB = new TextBox();
Label label = new Label();
addBT.setMediator(mediator);
list.setMediator(mediator);
cb.setMediator(mediator);
userNameTB.setMediator(mediator);
label.setMediator(mediator);
mediator.addButton = addBT;
mediator.list = list;
mediator.cb = cb;
mediator.userNameTextBox = userNameTB;
mediator.label = label;
addBT.changed();
System.out.println("-----------------------------");
list.changed();
}
}
编译并运行程序,输出结果如下:
--单击增加按钮--
列表框增加一项:张无忌。
组合框增加一项:张无忌。
客户信息增加成功后文本框清空。
文本标签内容改变,客户信息总数加1。
-----------------------------
--从列表框选择客户--
组合框选中项:小龙女。
文本框显示:小龙女。
由于在本实例中不同的组件类(即不同的同事类)所拥有的方法并不完全相同,因此中介者类没有针对抽象同事类编程,导致在具体中介者类中需要维持对具体同事类的引用,客户端代码无法完全透明地对待所有同事类和中介者类。
在某些情况下,如果设计得当,可以在客户端透明地对同事类和中介者类编程,这样系统将具有更好的灵活性和可扩展性。
在中介者模式的实际使用过程中,
如果需要引入新的具体同事类,只需要继承抽象同事类并实现其中的方法即可,由于具体同事类之间并无直接的引用关系,因此原有所有同事类无须进行任何修改,它们与新增同事对象之间的交互可以通过修改或者增加具体中介者类来实现;
如果需要在原有系统中增加新的具体中介者类,只需要继承抽象中介者类(或已有的具体中介者类)并覆盖其中定义的方法即可,在新的具体中介者中可以通过不同的方式来处理对象之间的交互,也可以增加对新增同事的引用和调用。
在客户端中只需要修改少许代码(如果引入配置文件的话有时可以不修改任何代码)就可以实现中介者的更换。
中介者模式将一个网状的系统结构变成一个以中介者对象为中心的星形结构,在这个星型结构中,使用中介者对象与其他对象的一对多关系来取代原有对象之间的多对多关系。中介者模式在事件驱动类软件中应用较为广泛,特别是基于GUI(Graphical User Interface,图形用户界面)的应用软件,此外,在类与类之间存在错综复杂的关联关系的系统中,中介者模式都能得到较好的应用。
(1) 中介者模式简化了对象之间的交互,它用中介者和同事的一对多交互代替了原来同事之间的多对多交互,一对多关系更容易理解、维护和扩展,将原本难以理解的网状结构转换成相对简单的星型结构。
(2) 中介者模式可将各同事对象解耦。中介者有利于各同事之间的松耦合,我们可以独立的改变和复用每一个同事和中介者,增加新的中介者和新的同事类都比较方便,更好地符合“开闭原则”。
(3) 可以减少子类生成,中介者将原本分布于多个对象间的行为集中在一起,改变这些行为只需生成新的中介者子类即可,这使各个同事类可被重用,无须对同事类进行扩展。
中介者模式的主要缺点如下:
在具体中介者类中包含了大量同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护。
在以下情况下可以考虑使用中介者模式:
(1) 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。
(2) 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象。
(3) 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则可以增加新的具体中介者类。
备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原,当前很多软件都提供了撤销(Undo)操作,其中就使用了备忘录模式。
备忘录模式(Memento Pattern):在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它是一种对象行为型模式,其别名为Token。

**● Originator(原发器):**它是一个普通类,可以创建一个备忘录,并存储它的当前内部状态,也可以使用备忘录来恢复其内部状态,一般将需要保存内部状态的类设计为原发器。
**●Memento(备忘录):**存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接供其他类使用,原发器的设计在不同的编程语言中实现机制会有所不同。
**●Caretaker(负责人):**负责人又称为管理者,它负责保存备忘录,但是不能对备忘录的内容进行操作或检查。在负责人类中可以存储一个或多个备忘录对象,它只负责存储对象,而不能修改对象,也无须知道对象的实现细节。
理解备忘录模式并不难,但关键在于如何设计备忘录类和负责人类。由于在备忘录中存储的是原发器的中间状态,因此需要防止原发器以外的其他对象访问备忘录,特别是不允许其他对象来修改备忘录。
下面我们通过简单的示例代码来说明如何使用Java语言实现备忘录模式:
在使用备忘录模式时,首先应该存在一个原发器类Originator,在真实业务中,原发器类是一个具体的业务类,它包含一些用于存储成员数据的属性,典型代码如下所示:
package dp.memento;
public class Originator {
private String state;
public Originator(){}
// 创建一个备忘录对象
public Memento createMemento() {
return new Memento(this);
}
// 根据备忘录对象恢复原发器状态
public void restoreMemento(Memento m) {
state = m.state;
}
public void setState(String state) {
this.state=state;
}
public String getState() {
return this.state;
}
}
对于备忘录类Memento而言,它通常提供了与原发器相对应的属性(可以是全部,也可以是部分)用于存储原发器的状态,典型的备忘录类设计代码如下:
package dp.memento;
//备忘录类,默认可见性,包内可见
class Memento {
private String state;
public Memento(Originator o) {
state = o.getState();
}
public void setState(String state) {
this.state=state;
}
public String getState() {
return this.state;
}
}
在设计备忘录类时需要考虑其封装性,除了Originator类,不允许其他类来调用备忘录类Memento的构造函数与相关方法。 如果不考虑封装性,允许其他类调用setState()等方法,将导致在备忘录中保存的历史状态发生改变,通过撤销操作所恢复的状态就不再是真实的历史状态,备忘录模式也就失去了本身的意义。
在使用Java语言实现备忘录模式时,一般通过将Memento类与Originator类定义在同一个包(package)中来实现封装,在Java语言中可使用默认访问标识符来定义Memento类,即保证其包内可见。
只有Originator类可以对Memento进行访问,而限制了其他类对Memento的访问。
在 Memento中保存了Originator的state值,如果Originator中的state值改变之后需撤销,可以通过调用它的restoreMemento()方法进行恢复。
对于负责人类Caretaker,它用于保存备忘录对象,并提供getMemento()方法用于向客户端返回一个备忘录对象,原发器通过使用这个备忘录对象可以回到某个历史状态。
典型的负责人类的实现代码如下:
package dp.memento;
public class Caretaker {
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento=memento;
}
}
在Caretaker类中不应该直接调用Memento中的状态改变方法,它的作用仅仅用于存储备忘录对象。将原发器备份生成的备忘录对象存储在其中,当用户需要对原发器进行恢复时再将存储在其中的备忘录对象取出。
Sunny软件公司欲开发一款可以运行在Android平台的触摸式中国象棋软件,由于考虑到有些用户是“菜鸟”,经常不小心走错棋;还有些用户因为不习惯使用手指在手机屏幕上拖动棋子,常常出现操作失误,因此该中国象棋软件要提供“悔棋”功能,用户走错棋或操作失误后可恢复到前一个步骤。如图所示:

如何实现“悔棋”功能是Sunny软件公司开发人员需要面对的一个重要问题,“悔棋”就是让系统恢复到某个历史状态,在很多软件中通常称之为“撤销”。下面我们来简单分析一下撤销功能的实现原理:
在实现撤销时,首先必须保存软件系统的历史状态,当用户需要取消错误操作并且返回到某个历史状态时,可以取出事先保存的历史状态来覆盖当前状态。

撤销功能示意图
备忘录模式正为解决此类撤销问题而诞生,它为我们的软件提供了“后悔药”,通过使用备忘录模式可以使系统恢复到某一特定的历史状态。
为了实现撤销功能,Sunny公司开发人员决定使用备忘录模式来设计中国象棋软件,其基本结构如图:

上图,Chessman充当原发器,ChessmanMemento充当备忘录,MementoCaretaker充当负责人,在MementoCaretaker中定义了一个ChessmanMemento类型的对象,用于存储备忘录。完整代码如下所示:
import lombok.AllArgsConstructor;
import lombok.Data;
//象棋棋子类:原发器
@Data
@AllArgsConstructor
class Chessman {
private String label;
private int x;
private int y;
//保存状态
public ChessmanMemento save() {
return new ChessmanMemento(this.label,this.x,this.y);
}
//恢复状态
public void restore(ChessmanMemento memento) {
this.label = memento.getLabel();
this.x = memento.getX();
this.y = memento.getY();
}
}
import lombok.AllArgsConstructor;
import lombok.Data;
//象棋棋子备忘录类:备忘录
@Data
@AllArgsConstructor
class ChessmanMemento {
private String label;
private int x;
private int y;
}
import lombok.Data;
//象棋棋子备忘录管理类:负责人
@Data
class MementoCaretaker {
private ChessmanMemento memento;
}
编写如下客户端测试代码:
class Client {
public static void main(String args[]) {
MementoCaretaker mc = new MementoCaretaker();
Chessman chess = new Chessman("车",1,1);
display(chess);
mc.setMemento(chess.save()); //保存状态
chess.setY(4);
display(chess);
mc.setMemento(chess.save()); //保存状态
display(chess);
chess.setX(5);
display(chess);
System.out.println("******悔棋******");
chess.restore(mc.getMemento()); //恢复状态
display(chess);
}
public static void display(Chessman chess) {
System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
}
}
编译并运行程序,输出结果如下:
棋子车当前位置为:第1行第1列。
棋子车当前位置为:第1行第4列。
棋子车当前位置为:第1行第4列。
棋子车当前位置为:第5行第4列。
******悔棋******
棋子车当前位置为:第1行第4列。
Sunny软件公司开发人员通过使用备忘录模式实现了中国象棋棋子的撤销操作,但是使用上述代码只能实现一次撤销,因为在负责人类中只定义一个备忘录对象来保存状态,后面保存的状态会将前一次保存的状态覆盖,但有时候用户需要撤销多步操作。如何实现多次撤销呢?本节将提供一种多次撤销的解决方案,那就是在负责人类中定义一个集合来存储多个备忘录,每个备忘录负责保存一个历史状态,在撤销时可以对备忘录集合进行逆向遍历,回到一个指定的历史状态,而且还可以对备忘录集合进行正向遍历,实现重做(Redo)操作,即取消撤销,让对象状态得到恢复。
改进之后的中国象棋棋子撤销功能结构图如图所示:

在上图,我们对负责人类MementoCaretaker进行了修改,在其中定义了一个ArrayList类型的集合对象来存储多个备忘录,其代码如下所示:
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
//象棋棋子备忘录管理类:负责人
@Data
class MementoCaretaker {
//定义一个集合来存储多个备忘录
private List<ChessmanMemento> mementolist = new ArrayList<>();
public ChessmanMemento getMemento(int i) {
return mementolist.get(i);
}
public void setMemento(ChessmanMemento memento) {
mementolist.add(memento);
}
}
编写如下客户端测试代码:
class Client {
private static int index = -1; //定义一个索引来记录当前状态所在位置
private static MementoCaretaker mc = new MementoCaretaker();
public static void main(String args[]) {
Chessman chess = new Chessman("车",1,1);
play(chess);
chess.setY(4);
play(chess);
chess.setX(5);
play(chess);
undo(chess,index);
undo(chess,index);
redo(chess,index);
redo(chess,index);
}
//下棋
public static void play(Chessman chess) {
mc.setMemento(chess.save()); //保存备忘录
index ++;
System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
}
//悔棋
public static void undo(Chessman chess,int i) {
System.out.println("******悔棋******");
index --;
chess.restore(mc.getMemento(i-1)); //撤销到上一个备忘录
System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
}
//撤销悔棋
public static void redo(Chessman chess,int i) {
System.out.println("******撤销悔棋******");
index ++;
chess.restore(mc.getMemento(i+1)); //恢复到下一个备忘录
System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
}
}
编译并运行程序,输出结果如下:
棋子车当前位置为:第1行第1列。
棋子车当前位置为:第1行第4列。
棋子车当前位置为:第5行第4列。
******悔棋******
棋子车当前位置为:第1行第4列。
******悔棋******
棋子车当前位置为:第1行第1列。
******撤销悔棋******
棋子车当前位置为:第1行第4列。
******撤销悔棋******
棋子车当前位置为:第5行第4列。
备忘录是一个很特殊的对象,只有原发器对它拥有控制的权力,负责人只负责管理,而其他类无法访问到备忘录,因此我们需要对备忘录进行封装。
为了实现对备忘录对象的封装,需要对备忘录的调用进行控制,对于原发器而言,它可以调用备忘录的所有信息,允许原发器访问返回到先前状态所需的所有数据;
对于负责人而言,只负责备忘录的保存并将备忘录传递给其他对象;
对于其他对象而言,只需要从负责人处取出备忘录对象并将原发器对象的状态恢复,而无须关心备忘录的保存细节。
理想的情况是只允许生成该备忘录的那个原发器访问备忘录的内部状态。
在实际开发中,原发器与备忘录之间的关系是非常特殊的,它们要分享信息而不让其他类知道,实现的方法因编程语言的不同而有所差异,在C++中可以使用friend关键字,让原发器类和备忘录类成为友元类,互相之间可以访问对象的一些私有的属性;
在Java语言中可以将原发器类和备忘录类放在一个包中,让它们之间满足默认的包内可见性,也可以将备忘录类作为原发器类的内部类,使得只有原发器才可以访问备忘录中的数据,其他对象都无法使用备忘录中的数据。
备忘录模式在很多软件的使用过程中普遍存在,但是在应用软件开发中,它的使用频率并不太高,因为现在很多基于窗体和浏览器的应用软件并没有提供撤销操作。如果需要为软件提供撤销功能,备忘录模式无疑是一种很好的解决方案。在一些字处理软件、图像编辑软件、数据库管理系统等软件中备忘录模式都得到了很好的应用。
备忘录模式的主要优点如下:
(1)它提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原。
(2)备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。备忘录保存了原发器的状态,采用列表、堆栈等集合来存储备忘录对象可以实现多次撤销操作。
备忘录模式的主要缺点如下:
资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源。
在以下情况下可以考虑使用备忘录模式:
(1)保存一个对象在某一个时刻的全部状态或部分状态,这样以后需要时它能够恢复到先前的状态,实现撤销操作。
(2)防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象。
观察者模式是使用频率最高的设计模式之一,它用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应作出反应。
在观察者模式中,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间可以没有任何相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。
观察者模式(Observer Pattern):定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式的别名包括发布-订阅(Publish/Subscribe)模式、模型\视图(Model/View)模式、源\监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。
观察者模式结构中通常包括观察目标和观察者两个继承层次结构

观察者模式结构图
● Subject(目标):目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify()。目标类可以是接口,也可以是抽象类或具体类。
● ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。
● Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。
● ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法。通常在实现时,可以调用具体目标类的attach()方法将自己添加到目标类的集合中或通过detach()方法将自己从目标类的集合中删除。
观察者模式描述了如何建立对象与对象之间的依赖关系,以及如何构造满足这种需求的系统。
观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。
作为对这个通知的响应,每个观察者都将监视观察目标的状态以使其状态与目标状态同步,这种交互也称为发布\订阅(Publish-Subscribe)。
观察目标是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅它并接收通知。
下面通过示意代码来对该模式进行进一步分析。首先我们定义一个抽象目标Subject,典型代码如下所示:
import java.util.*;
abstract class Subject {
//定义一个观察者集合用于存储所有观察者对象
protected ArrayList observers<Observer> = new ArrayList();
//注册方法,用于向观察者集合中增加一个观察者
public void attach(Observer observer) {
observers.add(observer);
}
//注销方法,用于在观察者集合中删除一个观察者
public void detach(Observer observer) {
observers.remove(observer);
}
//声明抽象通知方法
public abstract void notify();
}
具体目标类ConcreteSubject是实现了抽象目标类Subject的一个具体子类,其典型代码如下所示:
class ConcreteSubject extends Subject {
//实现通知方法
public void notify() {
//遍历观察者集合,调用每一个观察者的响应方法
for(Object obs:observers) {
((Observer)obs).update();
}
}
}
抽象观察者角色一般定义为一个接口,通常只声明一个update()方法,为不同观察者的更新(响应)行为定义相同的接口,这个方法在其子类中实现,不同的观察者具有不同的响应方法。抽象观察者Observer典型代码如下所示:
interface Observer {
//声明响应方法
public void update();
}
在具体观察者ConcreteObserver中实现了update()方法,其典型代码如下所示:
class ConcreteObserver implements Observer {
//实现响应方法
public void update() {
//具体响应代码
}
}
在有些更加复杂的情况下,具体观察者类ConcreteObserver的update()方法在执行时需要使用到具体目标类ConcreteSubject中的状态(属性),因此在ConcreteObserver与ConcreteSubject之间有时候还存在关联或依赖关系,在ConcreteObserver中定义一个ConcreteSubject实例,通过该实例获取存储在ConcreteSubject中的状态。
如果ConcreteObserver的update()方法不需要使用到ConcreteSubject中的状态属性,则可以对观察者模式的标准结构进行简化,在具体观察者ConcreteObserver和具体目标ConcreteSubject之间无须维持对象引用。
如果在具体层具有关联关系,系统的扩展性将受到一定的影响,增加新的具体目标类有时候需要修改原有观察者的代码,在一定程度上违反了“开闭原则”,但是如果原有观察者类无须关联新增的具体目标,则系统扩展性不受影响。
Sunny软件公司欲开发一款多人联机对战游戏(类似魔兽世界、星际争霸等游戏),在该游戏中,多个玩家可以加入同一战队组成联盟,当战队中某一成员受到敌人攻击时将给所有其他盟友发送通知,盟友收到通知后将作出响应。
Sunny软件公司开发人员需要提供一个设计方案来实现战队成员之间的联动。
Sunny软件公司开发人员通过对系统功能需求进行分析,发现在该系统中战队成员之间的联动过程可以简单描述如下:
联盟成员受到攻击–>发送通知给盟友–>盟友作出响应。
如果按照上述思路来设计系统,由于联盟成员在受到攻击时需要通知他的每一个盟友,因此每个联盟成员都需要持有其他所有盟友的信息,这将导致系统开销较大,因此Sunny公司开发人员决定引入一个新的角色——“战队控制中心”——来负责维护和管理每个战队所有成员的信息。
当一个联盟成员受到攻击时,将向相应的战队控制中心发送求助信息,战队控制中心再逐一通知每个盟友,盟友再作出响应

多人联机对战游戏中对象的联动
在上图中,受攻击的联盟成员将与战队控制中心产生联动,战队控制中心还将与其他盟友产生联动。
如何实现对象之间的联动?如何让一个对象的状态或行为改变时,依赖于它的对象能够得到通知并进行相应的处理?
为了实现对象之间的联动,Sunny软件公司开发人员决定使用观察者模式来进行多人联机对战游戏的设计,其基本结构如图:

多人联机对战游戏结构图
AllyControlCenter充当目标类,ConcreteAllyControlCenter充当具体目标类,Observer充当抽象观察者,Player充当具体观察者。完整代码如下所示:
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
//战队控制中心类:目标类
abstract class AllyControlCenter {
@Getter
@Setter
protected String allyName; //战队名称
protected ArrayList<Observer> players = new ArrayList<Observer>(); //定义一个集合用于存储战队成员
//注册方法
public void join(Observer obs) {
System.out.println(obs.getName() + "加入" + this.allyName + "战队!");
players.add(obs);
}
//注销方法
public void quit(Observer obs) {
System.out.println(obs.getName() + "退出" + this.allyName + "战队!");
players.remove(obs);
}
//声明抽象通知方法
public abstract void notifyObserver(String name);
}
//具体战队控制中心类:具体目标类
class ConcreteAllyControlCenter extends AllyControlCenter {
public ConcreteAllyControlCenter(String allyName) {
System.out.println(allyName + "战队组建成功!");
System.out.println("----------------------------");
this.allyName = allyName;
}
//实现通知方法
public void notifyObserver(String name) {
System.out.println(this.allyName + "战队紧急通知,盟友" + name + "遭受敌人攻击!");
//遍历观察者集合,调用每一个盟友(自己除外)的支援方法
for(Observer obs : players) {
if (!obs.getName().equalsIgnoreCase(name)) {
obs.help();
}
}
}
}
//抽象观察类
interface Observer {
public String getName();
public void setName(String name);
public void help(); //声明支援盟友方法
public void beAttacked(AllyControlCenter acc); //声明遭受攻击方法
}
import lombok.AllArgsConstructor;
import lombok.Data;
//战队成员类:具体观察者类
@Data
@AllArgsConstructor
class Player implements Observer {
private String name;
//支援盟友方法的实现
public void help() {
System.out.println("坚持住," + this.name + "来救你!");
}
//遭受攻击方法的实现,当遭受攻击时将调用战队控制中心类的通知方法notifyObserver()来通知盟友
public void beAttacked(AllyControlCenter acc) {
System.out.println(this.name + "被攻击!");
acc.notifyObserver(name);
}
}
编写如下客户端测试代码:
class Client {
public static void main(String args[]) {
//定义观察目标对象
AllyControlCenter acc;
acc = new ConcreteAllyControlCenter("金庸群侠");
//定义四个观察者对象
Observer player1,player2,player3,player4;
player1 = new Player("杨过");
acc.join(player1);
player2 = new Player("令狐冲");
acc.join(player2);
player3 = new Player("张无忌");
acc.join(player3);
player4 = new Player("段誉");
acc.join(player4);
//某成员遭受攻击
player1.beAttacked(acc);
}
}
编译并运行程序,输出结果如下:
金庸群侠战队组建成功!
----------------------------
杨过加入金庸群侠战队!
令狐冲加入金庸群侠战队!
张无忌加入金庸群侠战队!
段誉加入金庸群侠战队!
杨过被攻击!
金庸群侠战队紧急通知,盟友杨过遭受敌人攻击!
坚持住,令狐冲来救你!
坚持住,张无忌来救你!
坚持住,段誉来救你!
在本实例中,实现了两次对象之间的联动,当一个游戏玩家Player对象的beAttacked()方法被调用时,将调用AllyControlCenter的notifyObserver()方法来进行处理,而在notifyObserver()方法中又将调用其他Player对象的help()方法。Player的beAttacked()方法、AllyControlCenter的notifyObserver()方法以及Player的help()方法构成了一个联动触发链,执行顺序如下所示:
Player.beAttacked() –> AllyControlCenter.notifyObserver() –>Player.help()。
观察者模式在Java语言中的地位非常重要。在JDK的java.util包中,提供了Observable类以及Observer接口,它们构成了JDK对观察者模式的支持。

JDK提供的Observable类及Observer接口结构图
(1) Observer接口
在java.util.Observer接口中只声明一个方法,它充当抽象观察者,其方法声明代码如下所示:
void update(Observable o, Object arg);
当观察目标的状态发生变化时,该方法将会被调用,在Observer的子类中将实现update()方法,即具体观察者可以根据需要具有不同的更新行为。当调用观察目标类Observable的notifyObservers()方法时,将执行观察者类中的update()方法。
(2) Observable类
java.util.Observable类充当观察目标类,在Observable中定义了一个向量Vector来存储观察者对象,它所包含的方法及说明见表:
Observable类所包含方法及说明
| 方法名 | 方法描述 |
|---|---|
| Observable() | 构造方法,实例化Vector向量。 |
| addObserver(Observer o) | 用于注册新的观察者对象到向量中。 |
| deleteObserver(Observer o) | 用于删除向量中的某一个观察者对象。 |
| notifyObservers() | 通知方法,用于在方法内部循环调用向量中每一个观察者的update()方法。 |
| deleteObservers() | 用于清空向量,即删除向量中所有观察者对象。 |
| setChanged() | 该方法被调用后会设置一个boolean类型的内部标记变量changed的值为true,表示观察目标对象的状态发生了变化。 |
| clearChanged() | 用于将changed变量的值设为false,表示对象状态不再发生改变或者已经通知了所有的观察者对象,调用了它们的update()方法。 |
| hasChanged() | 用于测试对象状态是否改变。 |
| countObservers() | 用于返回向量中观察者的数量。 |
我们可以直接使用Observer接口和Observable类来作为观察者模式的抽象层,再自定义具体观察者类和具体观察目标类,通过使用JDK中的Observer接口和Observable类,可以更加方便地在Java语言中应用观察者模式。
JDK 1.0及更早版本的事件模型基于职责链模式,但是这种模型不适用于复杂的系统,因此在JDK 1.1及以后的各个版本中,事件处理模型采用基于观察者模式的委派事件模型(DelegationEvent Model, DEM),即一个Java组件所引发的事件并不由引发事件的对象自己来负责处理,而是委派给独立的事件处理对象负责。
在DEM模型中,目标角色(如界面组件)负责发布事件,而观察者角色(事件处理者)可以向目标订阅它所感兴趣的事件。
当一个具体目标产生一个事件时,它将通知所有订阅者。
事件的发布者称为事件源(Event Source),而订阅者称为事件监听器(Event Listener),在这个过程中还可以通过**事件对象(Event Object)**来传递与事件相关的信息,可以在事件监听者的实现类中实现事件处理,因此事件监听对象又可以称为事件处理对象。
事件源对象、事件监听对象(事件处理对象)和事件对象构成了Java事件处理模型的三要素。
事件源对象充当观察目标,而事件监听对象充当观察者。
以按钮点击事件为例,其事件处理流程如下:
(1) 如果用户在GUI中单击一个按钮,将触发一个事件(如ActionEvent类型的动作事件),JVM将产生一个相应的ActionEvent类型的事件对象,在该事件对象中包含了有关事件和事件源的信息,此时按钮是事件源对象;
(2) 将ActionEvent事件对象传递给事件监听对象(事件处理对象),JDK提供了专门用于处理ActionEvent事件的接口ActionListener,开发人员需提供一个ActionListener的实现类(如MyActionHandler),实现在ActionListener接口中声明的抽象事件处理方法actionPerformed(),对所发生事件做出相应的处理;
(3) 开发人员将ActionListener接口的实现类(如MyActionHandler)对象注册到按钮中,可以通过按钮类的addActionListener()方法来实现注册;
(4) JVM在触发事件时将调用按钮的fireXXX()方法,在该方法内部将调用注册到按钮中的事件处理对象的actionPerformed()方法,实现对事件的处理。
使用类似的方法,我们可自定义GUI组件,如包含两个文本框和两个按钮的登录组件LoginBean,可以采用如图22-6所示设计方案:

图自定义登录组件结构图【省略按钮、文本框等界面组件】
上图相关类说明如下:
(1) LoginEvent是事件类,它用于封装与事件有关的信息,它不是观察者模式的一部分,但是它可以在目标对象和观察者对象之间传递数据,在AWT事件模型中,所有的自定义事件类都是java.util.EventObject的子类。
(2) LoginEventListener充当抽象观察者,它声明了事件响应方法validateLogin(),用于处理事件,该方法也称为事件处理方法,validateLogin()方法将一个LoginEvent类型的事件对象作为参数,用于传输与事件相关的数据,在其子类中实现该方法,实现具体的事件处理。
(3) LoginBean充当具体目标类,在这里我们没有定义抽象目标类,对观察者模式进行了一定的简化。在LoginBean中定义了抽象观察者LoginEventListener类型的对象lel和事件对象LoginEvent,提供了注册方法addLoginEventListener()用于添加观察者,在Java事件处理中,通常使用的是一对一的观察者模式,而不是一对多的观察者模式,也就是说,一个观察目标中只定义一个观察者对象,而不是提供一个观察者对象的集合。在LoginBean中还定义了通知方法fireLoginEvent(),该方法在Java事件处理模型中称为“点火方法”,在该方法内部实例化了一个事件对象LoginEvent,将用户输入的信息传给观察者对象,并且调用了观察者对象的响应方法validateLogin()。
(4) LoginValidatorA和LoginValidatorB充当具体观察者类,它们实现了在LoginEventListener接口中声明的抽象方法validateLogin(),用于具体实现事件处理,该方法包含一个LoginEvent类型的参数,在LoginValidatorA和LoginValidatorB类中可以针对相同的事件提供不同的实现。
在当前流行的MVC(Model-View-Controller)架构中也应用了观察者模式,MVC是一种架构模式,它包含三个角色:模型(Model),视图(View)和控制器(Controller)。其中模型可对应于观察者模式中的观察目标,而视图对应于观察者,控制器可充当两者之间的中介者。当模型层的数据发生改变时,视图层将自动改变其显示内容。如图22-7所示:

MVC结构示意图
在MVC结构示意图中,模型层提供的数据是视图层所观察的对象,在视图层中包含两个用于显示数据的图表对象,一个是柱状图,一个是饼状图,相同的数据拥有不同的图表显示方式,
如果模型层的数据发生改变,两个图表对象将随之发生变化,这意味着图表对象依赖模型层提供的数据对象,因此数据对象的任何状态改变都应立即通知它们。
同时,这两个图表之间相互独立,不存在任何联系,而且图表对象的个数没有任何限制,用户可以根据需要再增加新的图表对象,如折线图。
在增加新的图表对象时,无须修改原有类库,满足“开闭原则”。
大家可以查阅相关资料对MVC模式进行深入学习,如Oracle公司提供的技术文档《Java SE Application Design With MVC》,参考链接:http://www.oracle.com/technetwork/articles/javase/index-142890.html。
观察者模式是一种使用频率非常高的设计模式,无论是移动应用、Web应用或者桌面应用,观察者模式几乎无处不在,它为实现对象之间的联动提供了一套完整的解决方案,凡是涉及到一对一或者一对多的对象交互场景都可以使用观察者模式。观察者模式广泛应用于各种编程语言的GUI事件处理的实现,在基于事件的XML解析技术(如SAX2)以及Web事件处理中也都使用了观察者模式。
观察者模式的主要优点如下:
(1) 观察者模式可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层充当具体观察者角色。
(2) 观察者模式在观察目标和观察者之间建立一个抽象的耦合。观察目标只需要维持一个抽象观察者的集合,无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。
(3) 观察者模式支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度。
(4) 观察者模式满足“开闭原则”的要求,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便。
观察者模式的主要缺点如下:
(1) 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间。
(2) 如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
(3) 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
在以下情况下可以考虑使用观察者模式:
(1) 一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用。
(2) 一个对象的改变将导致一个或多个其他对象也发生改变,而并不知道具体有多少对象将发生改变,也不知道这些对象是谁。
(3) 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。
“人有悲欢离合,月有阴晴圆缺”,包括人在内,很多事物都具有多种状态,而且在不同状态下会具有不同的行为,这些状态在特定条件下还将发生相互转换。就像水,它可以凝固成冰,也可以受热蒸发后变成水蒸汽,水可以流动,冰可以雕刻,蒸汽可以扩散。我们可以用UML状态图来描述H2O的三种状态,如图1所示:

H2O的三种状态(未考虑临界点)
在软件系统中,有些对象也像水一样具有多种状态,这些状态在某些情况下能够相互转换,而且对象在不同的状态下也将具有不同的行为。为了更好地对这些具有多种状态的对象进行设计,我们可以使用一种被称之为状态模式的设计模式
状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式。状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化,对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致处理。
状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。
在状态模式中引入了抽象状态类和具体状态类,它们是状态模式的核心,其结构

状态模式结构图
**● Context(环境类):**环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State的实例,这个实例定义当前状态,在具体实现时,它是一个State子类的对象。
**● State(抽象状态类):**它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。
**● ConcreteState(具体状态类):**它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。
在状态模式中,我们将对象在不同状态下的行为封装到不同的状态类中,为了让系统具有更好的灵活性和可扩展性,同时对各状态下的共有行为进行封装,我们需要对状态进行抽象,引入了抽象状态类角色,其典型代码如下所示:
abstract class State {
//声明抽象业务方法,不同的具体状态类可以不同的实现
public abstract void handle();
}
在抽象状态类的子类即具体状态类中实现了在抽象状态类中声明的业务方法,不同的具体状态类可以提供完全不同的方法实现,在实际使用时,在一个状态类中可能包含多个业务方法,如果在具体状态类中某些业务方法的实现完全相同,可以将这些方法移至抽象状态类,实现代码的复用,典型的具体状态类代码如下所示:
class ConcreteState extends State {
public void handle() {
//方法具体实现代码
}
}
环境类维持一个对抽象状态类的引用,通过setState()方法可以向环境类注入不同的状态对象,再在环境类的业务方法中调用状态对象的方法,典型代码如下所示:
class Context {
private State state; //维持一个对抽象状态对象的引用
private int value; //其他属性值,该属性值的变化可能会导致对象状态发生变化
//设置状态对象
public void setState(State state) {
this.state = state;
}
public void request() {
//其他代码
state.handle(); //调用状态对象的业务方法
//其他代码
}
}
环境类实际上是真正拥有状态的对象,我们只是将环境类中与状态有关的代码提取出来封装到专门的状态类中。在状态模式结构图中,环境类Context与抽象状态类State之间存在单向关联关系,在Context中定义了一个State对象。在实际使用时,它们之间可能存在更为复杂的关系,State与Context之间可能也存在依赖或者关联关系。
在状态模式的使用过程中,一个对象的状态之间还可以进行相互转换,通常有两种实现状态转换的方式:
(1) 统一由环境类来负责状态之间的转换,此时,环境类还充当了状态管理器(State Manager)角色,在环境类的业务方法中通过对某些属性值的判断实现状态转换,还可以提供一个专门的方法用于实现属性判断和状态转换,如下代码片段所示:
……
public void changeState() {
//判断属性值,根据属性值进行状态转换
if (value == 0) {
this.setState(new ConcreteStateA());
}
else if (value == 1) {
this.setState(new ConcreteStateB());
}
......
}
……
(2) 由具体状态类来负责状态之间的转换,可以在具体状态类的业务方法中判断环境类的某些属性值再根据情况为环境类设置新的状态对象,实现状态转换,同样,也可以提供一个专门的方法来负责属性值的判断和状态转换。此时,状态类与环境类之间就将存在依赖或关联关系,因为状态类需要访问环境类中的属性值,如下代码片段所示:
……
public void changeState(Context ctx) {
//根据环境对象中的属性值进行状态转换
if (ctx.getValue() == 1) {
ctx.setState(new ConcreteStateB());
}
else if (ctx.getValue() == 2) {
ctx.setState(new ConcreteStateC());
}
......
}
……
Sunny软件公司欲为某银行开发一套信用卡业务系统,银行账户(Account)是该系统的核心类之一,通过分析,Sunny软件公司开发人员发现在该系统中,账户存在三种状态,且在不同状态下账户存在不同的行为,具体说明如下:
(1) 如果账户中余额大于等于0,则账户的状态为正常状态(Normal State),此时用户既可以向该账户存款也可以从该账户取款;
(2) 如果账户中余额小于0,并且大于-2000,则账户的状态为透支状态(Overdraft State),此时用户既可以向该账户存款也可以从该账户取款,但需要按天计算利息;
(3) 如果账户中余额等于-2000,那么账户的状态为受限状态(Restricted State),此时用户只能向该账户存款,不能再从中取款,同时也将按天计算利息;
(4) 根据余额的不同,以上三种状态可发生相互转换。
Sunny软件公司开发人员对银行账户类进行分析,绘制了如图2所示UML状态图:

银行账户状态图
NormalState表示正常状态,OverdraftState表示透支状态,RestrictedState表示受限状态,在这三种状态下账户对象拥有不同的行为,方法deposit()用于存款,withdraw()用于取款,computeInterest()用于计算利息,stateCheck()用于在每一次执行存款和取款操作后根据余额来判断是否要进行状态转换并实现状态转换,相同的方法在不同的状态中可能会有不同的实现。为了实现不同状态下对象的各种行为以及对象状态之间的相互转换,Sunny软件公司开发人员设计了一个较为庞大的账户类Account,其中部分代码如下所示:
class Account {
private String state; //状态
private int balance; //余额
......
//存款操作
public void deposit() {
//存款
stateCheck();
}
//取款操作
public void withdraw() {
if (state.equalsIgnoreCase("NormalState") || state.equalsIgnoreCase("OverdraftState ")) {
//取款
stateCheck();
}
else {
//取款受限
}
}
//计算利息操作
public void computeInterest() {
if(state.equalsIgnoreCase("OverdraftState") || state.equalsIgnoreCase("RestrictedState ")) {
//计算利息
}
}
//状态检查和转换操作
public void stateCheck() {
if (balance >= 0) {
state = "NormalState";
}
else if (balance > -2000 && balance < 0) {
state = "OverdraftState";
}
else if (balance == -2000) {
state = "RestrictedState";
}
else if (balance < -2000) {
//操作受限
}
}
......
}
分析上述代码,我们不难发现存在如下几个问题:
(1) 几乎每个方法中都包含状态判断语句,以判断在该状态下是否具有该方法以及在特定状态下该方法如何实现,导致代码非常冗长,可维护性较差;
(2) 拥有一个较为复杂的stateCheck()方法,包含大量的if…else if…else…语句用于进行状态转换,代码测试难度较大,且不易于维护;
(3) 系统扩展性较差,如果需要增加一种新的状态,如冻结状态(Frozen State,在该状态下既不允许存款也不允许取款),需要对原有代码进行大量修改,扩展起来非常麻烦。
为了解决这些问题,我们可以使用状态模式,在状态模式中,我们将对象在每一个状态下的行为和状态转移语句封装在一个个状态类中,通过这些状态类来分散冗长的条件转移语句,让系统具有更好的灵活性和可扩展性,状态模式可以在一定程度上解决上述问题。
Sunny软件公司开发人员使用状态模式来解决账户状态的转换问题,客户端只需要执行简单的存款和取款操作,系统根据余额将自动转换到相应的状态,其基本结构如所示:

银行账户结构图
Account充当环境类角色,AccountState充当抽象状态角色,NormalState、OverdraftState和RestrictedState充当具体状态角色。完整代码如下所示:
import lombok.Getter;
import lombok.Setter;
//银行账户:环境类
class Account {
@Setter
private AccountState state; //维持一个对抽象状态对象的引用
private final String owner; //开户名
@Getter
@Setter
private double balance = 0; //账户余额
public Account(String owner,double init) {
this.owner = owner;
this.balance = balance;
this.state = new NormalState(this); //设置初始状态
System.out.println(this.owner + "开户,初始金额为" + init);
System.out.println("---------------------------------------------");
}
public void deposit(double amount) {
System.out.println(this.owner + "存款" + amount);
state.deposit(amount); //调用状态对象的deposit()方法
System.out.println("现在余额为"+ this.balance);
System.out.println("现在帐户状态为"+ this.state.getClass().getName());
System.out.println("---------------------------------------------");
}
public void withdraw(double amount) {
System.out.println(this.owner + "取款" + amount);
state.withdraw(amount); //调用状态对象的withdraw()方法
System.out.println("现在余额为"+ this.balance);
System.out.println("现在帐户状态为"+ this. state.getClass().getName());
System.out.println("---------------------------------------------");
}
public void computeInterest()
{
state.computeInterest(); //调用状态对象的computeInterest()方法
}
}
//抽象状态类
abstract class AccountState {
protected Account acc;
public abstract void deposit(double amount);
public abstract void withdraw(double amount);
public abstract void computeInterest();
public abstract void stateCheck();
}
// 正常状态
class NormalState extends AccountState {
public NormalState(Account acc) {
this.acc = acc;
}
public NormalState(AccountState state) {
this.acc = state.acc;
}
public void deposit(double amount) {
acc.setBalance(acc.getBalance() + amount);
stateCheck();
}
public void withdraw(double amount) {
acc.setBalance(acc.getBalance() - amount);
stateCheck();
}
public void computeInterest()
{
System.out.println("正常状态,无须支付利息!");
}
//状态转换
public void stateCheck() {
if (acc.getBalance() > -2000 && acc.getBalance() <= 0) {
acc.setState(new OverdraftState(this));
}
else if (acc.getBalance() == -2000) {
acc.setState(new RestrictedState(this));
}
else if (acc.getBalance() < -2000) {
System.out.println("操作受限!");
}
}
}
//透支状态:具体状态类
class OverdraftState extends AccountState
{
public OverdraftState(AccountState state) {
this.acc = state.acc;
}
public void deposit(double amount) {
acc.setBalance(acc.getBalance() + amount);
stateCheck();
}
public void withdraw(double amount) {
acc.setBalance(acc.getBalance() - amount);
stateCheck();
}
public void computeInterest() {
System.out.println("计算利息!");
}
//状态转换
public void stateCheck() {
if (acc.getBalance() > 0) {
acc.setState(new NormalState(this));
}
else if (acc.getBalance() == -2000) {
acc.setState(new RestrictedState(this));
}
else if (acc.getBalance() < -2000) {
System.out.println("操作受限!");
}
}
}
//受限状态:具体状态类
class RestrictedState extends AccountState {
public RestrictedState(AccountState state) {
this.acc = state.acc;
}
public void deposit(double amount) {
acc.setBalance(acc.getBalance() + amount);
stateCheck();
}
public void withdraw(double amount) {
System.out.println("帐号受限,取款失败");
}
public void computeInterest() {
System.out.println("计算利息!");
}
//状态转换
public void stateCheck() {
if(acc.getBalance() > 0) {
acc.setState(new NormalState(this));
}
else if(acc.getBalance() > -2000) {
acc.setState(new OverdraftState(this));
}
}
}
编写如下客户端测试代码:
class Client {
public static void main(String[] args) {
Account acc = new Account("段誉",0.0);
acc.deposit(1000);
acc.withdraw(2000);
acc.deposit(3000);
acc.withdraw(4000);
acc.withdraw(1000);
acc.computeInterest();
}
}
编译并运行程序,输出结果如下:
段誉开户,初始金额为0.0
---------------------------------------------
段誉存款1000.0
现在余额为1000.0
现在帐户状态为org.example.demo01.NormalState
---------------------------------------------
段誉取款2000.0
现在余额为-1000.0
现在帐户状态为org.example.demo01.OverdraftState
---------------------------------------------
段誉存款3000.0
现在余额为2000.0
现在帐户状态为org.example.demo01.NormalState
---------------------------------------------
段誉取款4000.0
现在余额为-2000.0
现在帐户状态为org.example.demo01.RestrictedState
---------------------------------------------
段誉取款1000.0
帐号受限,取款失败
现在余额为-2000.0
现在帐户状态为org.example.demo01.RestrictedState
---------------------------------------------
计算利息!
在有些情况下,多个环境对象可能需要共享同一个状态,如果希望在系统中实现多个环境对象共享一个或多个状态对象,那么需要将这些状态对象定义为环境类的静态成员对象。
下面通过一个简单实例来说明如何实现共享状态:
如果某系统要求两个开关对象要么都处于开的状态,要么都处于关的状态,在使用时它们的状态必须保持一致,开关可以由开转换到关,也可以由关转换到开。
可以使用状态模式来实现开关的设计,其结构如图所示:

开关及其状态设计结构图
//开关类Switch代码
class Switch {
private static State state,onState,offState; //定义三个静态的状态对象
private String name;
public Switch(String name) {
this.name = name;
onState = new OnState();
offState = new OffState();
this.state = onState;
}
public void setState(State state) {
this.state = state;
}
public static State getState(String type) {
if (type.equalsIgnoreCase("on")) {
return onState;
}
else {
return offState;
}
}
//打开开关
public void on() {
System.out.print(name);
state.on(this);
}
//关闭开关
public void off() {
System.out.print(name);
state.off(this);
}
}
//抽象状态类如下代码
abstract class State {
public abstract void on(Switch s);
public abstract void off(Switch s);
}
//打开状态
class OnState extends State {
public void on(Switch s) {
System.out.println("已经打开!");
}
public void off(Switch s) {
System.out.println("关闭!");
s.setState(Switch.getState("off"));
}
}
//关闭状态
class OffState extends State {
public void on(Switch s) {
System.out.println("打开!");
s.setState(Switch.getState("on"));
}
public void off(Switch s) {
System.out.println("已经关闭!");
}
}
编写如下客户端代码进行测试:
class Client {
public static void main(String args[]) {
Switch s1,s2;
s1=new Switch("开关1");
s2=new Switch("开关2");
s1.on();
s2.on();
s1.off();
s2.off();
s2.on();
s1.on();
}
}
输出结果如下:
开关1已经打开!
开关2已经打开!
开关1关闭!
开关2已经关闭!
开关2打开!
开关1已经打开!
在状态模式中实现状态转换时,具体状态类可通过调用环境类Context的setState()方法进行状态的转换操作,也可以统一由环境类Context来实现状态的转换。此时,增加新的具体状态类可能需要修改其他具体状态类或者环境类的源代码,否则系统无法转换到新增状态。但是对于客户端来说,无须关心状态类,可以为环境类设置默认的状态类,而将状态的转换工作交给具体状态类或环境类来完成,具体的转换细节对于客户端而言是透明的。
在上面的“银行账户状态转换”实例中,我们通过具体状态类来实现状态的转换,在每一个具体状态类中都包含一个stateCheck()方法,在该方法内部实现状态的转换,除此之外,我们还可以通过环境类来实现状态转换,环境类作为一个状态管理器,统一实现各种状态之间的转换操作。
下面通过一个包含循环状态的简单实例来说明如何使用环境类实现状态转换:
Sunny软件公司某开发人员欲开发一个屏幕放大镜工具,其具体功能描述如下:
用户单击“放大镜”按钮之后屏幕将放大一倍,再点击一次“放大镜”按钮屏幕再放大一倍,第三次点击该按钮后屏幕将还原到默认大小。
可以考虑使用状态模式来设计该屏幕放大镜工具,我们定义三个屏幕状态类NormalState、LargerState和LargestState来对应屏幕的三种状态,分别是正常状态、二倍放大状态和四倍放大状态,屏幕类Screen充当环境类,其结构如图6所示:

屏幕放大镜工具结构图
本实例核心代码如下所示:
//抽象状态类
abstract class State {
public abstract void display();
}
//屏幕类
class Screen {
//枚举所有的状态,currentState表示当前状态
private State currentState, normalState, largerState, largestState;
public Screen() {
this.normalState = new NormalState(); //创建正常状态对象
this.largerState = new LargerState(); //创建二倍放大状态对象
this.largestState = new LargestState(); //创建四倍放大状态对象
this.currentState = normalState; //设置初始状态
this.currentState.display();
}
public void setState(State state) {
this.currentState = state;
}
//单击事件处理方法,封转了对状态类中业务方法的调用和状态的转换
public void onClick() {
if (this.currentState == normalState) {
this.setState(largerState);
this.currentState.display();
}
else if (this.currentState == largerState) {
this.setState(largestState);
this.currentState.display();
}
else if (this.currentState == largestState) {
this.setState(normalState);
this.currentState.display();
}
}
}
//正常状态类
class NormalState extends State{
public void display() {
System.out.println("正常大小!");
}
}
//二倍状态类
class LargerState extends State{
public void display() {
System.out.println("二倍大小!");
}
}
//四倍状态类
class LargestState extends State{
public void display() {
System.out.println("四倍大小!");
}
}
在上述代码中,所有的状态转换操作都由环境类Screen来实现,此时,环境类充当了状态管理器角色。如果需要增加新的状态,例如“八倍状态类”,需要修改环境类,这在一定程度上违背了“开闭原则”,但对其他状态类没有任何影响。
编写如下客户端代码进行测试:
class Client {
public static void main(String[] args) {
Screen screen = new Screen();
screen.onClick();
screen.onClick();
screen.onClick();
}
}
输出结果如下:
正常大小!
二倍大小!
四倍大小!
正常大小!
状态模式将一个对象在不同状态下的不同行为封装在一个个状态类中,通过设置不同的状态对象可以让环境对象拥有不同的行为,而状态转换的细节对于客户端而言是透明的,方便了客户端的使用。在实际开发中,状态模式具有较高的使用频率,在工作流和游戏开发中状态模式都得到了广泛的应用,例如公文状态的转换、游戏中角色的升级等。
状态模式的主要优点如下:
(1) 封装了状态的转换规则,在状态模式中可以将状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中。
(2) 将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为。
(3) 允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,状态模式可以让我们避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起。
(4) 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
状态模式的主要缺点如下:
(1) 状态模式的使用必然会增加系统中类和对象的个数,导致系统运行开销增大。
(2) 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,增加系统设计的难度。
(3) 状态模式对“开闭原则”的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。
在以下情况下可以考虑使用状态模式:
(1) 对象的行为依赖于它的状态(如某些属性值),状态的改变将导致行为的变化。
(2) 在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强。
在策略模式中,我们可以定义一些独立的类来封装不同的算法,每一个类封装一种具体的算法,在这里,每一个封装算法的类我们都可以称之为一种策略(Strategy),为了保证这些策略在使用时具有一致性,一般会提供一个抽象的策略类来做规则的定义,而每种算法则对应于一个具体策略类。
策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类中,每一个策略类封装了一种实现算法,使用算法的环境类针对抽象策略类进行编程,符合“依赖倒转原则”。在出现新的算法时,只需要增加一个新的实现了抽象策略类的具体策略类即可。
策略模式(Strategy Pattern):定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。策略模式是一种对象行为型模式。
策略模式结构并不复杂,但我们需要理解其中环境类Context的作用,其结构如图24-1所示:

● Context**(环境类):**环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。
● Strategy**(抽象策略类):**它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。
● ConcreteStrategy**(具体策略类):**它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。
思考
一个环境类Context能否对应多个不同的策略等级结构?如何设计?
策略模式是一个比较容易理解和使用的设计模式,策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。策略模式通常把一个系列的算法封装到一系列具体策略类里面,作为抽象策略类的子类。在策略模式中,对环境类和抽象策略类的理解非常重要,环境类是需要使用算法的类。在一个系统中可以存在多个环境类,它们可能需要重用一些相同的算法。
在使用策略模式时,我们需要将算法从Context类中提取出来,首先应该创建一个抽象策略类,其典型代码如下所示:
abstract class AbstractStrategy {
public abstract void algorithm(); //声明抽象算法
}
然后再将封装每一种具体算法的类作为该抽象策略类的子类,如下代码所示:
class ConcreteStrategyA extends AbstractStrategy {
//算法的具体实现
public void algorithm() {
//算法A
}
}
其他具体策略类与之类似,对于Context类而言,在它与抽象策略类之间建立一个关联关系,其典型代码如下所示:
class Context {
private AbstractStrategy strategy; //维持一个对抽象策略类的引用
public void setStrategy(AbstractStrategy strategy) {
this.strategy= strategy;
}
//调用策略类中的算法
public void algorithm() {
strategy.algorithm();
}
}
在Context类中定义一个AbstractStrategy类型的对象strategy,通过注入的方式在客户端传入一个具体策略对象,客户端代码片段如下所示:
……
Context context = new Context();
AbstractStrategy strategy;
strategy = new ConcreteStrategyA(); //可在运行时指定类型
context.setStrategy(strategy);
context.algorithm();
……
在客户端代码中只需注入一个具体策略对象,可以将具体策略类类名存储在配置文件中,通过反射来动态创建具体策略对象,从而使得用户可以灵活地更换具体策略类,增加新的具体策略类也很方便。策略模式提供了一种可插入式(Pluggable)算法的实现方案。
Sunny软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下:
(1) 学生凭学生证可享受票价8折优惠;
(2) 年龄在10周岁及以下的儿童可享受每张票减免10元的优惠(原始票价需大于等于20元);
(3) 影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品。
该系统在将来可能还要根据需要引入新的打折方式。
为了实现上述电影票打折功能,Sunny软件公司开发人员设计了一个电影票类MovieTicket,其核心代码片段如下所示:
//电影票类
class MovieTicket {
private double price; //电影票价格
private String type; //电影票类型
public void setPrice(double price) {
this.price = price;
}
public void setType(String type) {
this.type = type;
}
public double getPrice() {
return this.calculate();
}
//计算打折之后的票价
public double calculate() {
//学生票折后票价计算
if(this.type.equalsIgnoreCase("student")) {
System.out.println("学生票:");
return this.price * 0.8;
}
//儿童票折后票价计算
else if(this.type.equalsIgnoreCase("children") && this.price >= 20 ) {
System.out.println("儿童票:");
return this.price - 10;
}
//VIP票折后票价计算
else if(this.type.equalsIgnoreCase("vip")) {
System.out.println("VIP票:");
System.out.println("增加积分!");
return this.price * 0.5;
}
else {
return this.price; //如果不满足任何打折要求,则返回原始票价
}
}
}
编写如下客户端测试代码:
class Client {
public static void main(String args[]) {
MovieTicket mt = new MovieTicket();
double originalPrice = 60.0; //原始票价
double currentPrice; //折后价
mt.setPrice(originalPrice);
System.out.println("原始价为:" + originalPrice);
System.out.println("---------------------------------");
mt.setType("student"); //学生票
currentPrice = mt.getPrice();
System.out.println("折后价为:" + currentPrice);
System.out.println("---------------------------------");
mt.setType("children"); //儿童票
currentPrice = mt.getPrice();
System.out.println("折后价为:" + currentPrice);
}
}
编译并运行程序,输出结果如下所示:
原始价为:60.0
---------------------------------
学生票:
折后价为:48.0
---------------------------------
儿童票:
折后价为:50.0
通过MovieTicket类实现了电影票的折后价计算,该方案解决了电影票打折问题,每一种打折方式都可以称为一种打折算法,更换打折方式只需修改客户端代码中的参数,无须修改已有源代码,但该方案并不是一个完美的解决方案,它至少存在如下三个问题:
(1) MovieTicket类的calculate()方法非常庞大,它包含各种打折算法的实现代码,在代码中出现了较长的if…else…语句,不利于测试和维护。
(2) 增加新的打折算法或者对原有打折算法进行修改时必须修改MovieTicket类的源代码,违反了“开闭原则”,系统的灵活性和可扩展性较差。
(3) 算法的复用性差,如果在另一个系统(如商场销售管理系统)中需要重用某些打折算法,只能通过对源代码进行复制粘贴来重用,无法单独重用其中的某个或某些算法(重用较为麻烦)。
如何解决这三个问题?导致产生这些问题的主要原因在于MovieTicket类职责过重,它将各种打折算法都定义在一个类中,这既不便于算法的重用,也不便于算法的扩展。
因此我们需要对MovieTicket类进行重构,将原本庞大的MovieTicket类的职责进行分解,将算法的定义和使用分离,这就是策略模式所要解决的问题
为了实现打折算法的复用,并能够灵活地向系统中增加新的打折方式,Sunny软件公司开发人员使用策略模式对电影院打折方案进行重构,重构后基本结构如图24-2所示:

MovieTicket充当环境类角色,Discount充当抽象策略角色,StudentDiscount、 ChildrenDiscount 和VIPDiscount充当具体策略角色。完整代码如下所示:
//电影票类:环境类
class MovieTicket {
private double price;
private Discount discount; //维持一个对抽象折扣类的引用
public void setPrice(double price) {
this.price = price;
}
//注入一个折扣类对象
public void setDiscount(Discount discount) {
this.discount = discount;
}
public double getPrice() {
//调用折扣类的折扣价计算方法
return discount.calculate(this.price);
}
}
//折扣类:抽象策略类
interface Discount {
public double calculate(double price);
}
//学生票折扣类:具体策略类
class StudentDiscount implements Discount {
public double calculate(double price) {
System.out.println("学生票:");
return price * 0.8;
}
}
//儿童票折扣类:具体策略类
class ChildrenDiscount implements Discount {
public double calculate(double price) {
System.out.println("儿童票:");
return price - 10;
}
}
//VIP会员票折扣类:具体策略类
class VIPDiscount implements Discount {
public double calculate(double price) {
System.out.println("VIP票:");
System.out.println("增加积分!");
return price * 0.5;
}
}
编写如下客户端测试代码:
class Client {
public static void main(String args[]) {
MovieTicket mt = new MovieTicket();
double originalPrice = 60.0;
double currentPrice;
mt.setPrice(originalPrice);
System.out.println("原始价为:" + originalPrice);
System.out.println("---------------------------------");
Discount discount = new StudentDiscount();
mt.setDiscount(discount); //注入折扣对象
currentPrice = mt.getPrice();
System.out.println("折后价为:" + currentPrice);
}
}
编译并运行程序,输出结果如下:
原始价为:60.0
---------------------------------
学生票:
折后价为:48.0
如果需要增加新的打折方式,原有代码均无须修改,只要增加一个新的折扣类作为抽象折扣类的子类,实现在抽象折扣类中声明的打折方法,将原有具体折扣类类名改为新增折扣类类名即可,完全符合“开闭原则”。
策略模式实用性强、扩展性好,在软件开发中得以广泛使用,是使用频率较高的设计模式之一。
下面将介绍策略模式的两个典型应用实例,一个来源于Java SE,一个来源于微软公司推出的演示项目PetShop。
(1) Java SE的容器布局管理就是策略模式的一个经典应用实例,其基本结构示意图如图:

在Java SE开发中,用户需要对容器对象Container中的成员对象如按钮、文本框等GUI控件进行布局(Layout),在程序运行期间由客户端动态决定一个Container对象如何布局,Java语言在JDK中提供了几种不同的布局方式,封装在不同的类中,如BorderLayout、FlowLayout、GridLayout、GridBagLayout和CardLayout等。在上图,Container类充当环境角色Context,而LayoutManager作为所有布局类的公共父类扮演了抽象策略角色,它给出所有具体布局类所需的接口,而具体策略类是LayoutManager的子类,也就是各种具体的布局类,它们封装了不同的布局方式。
任何人都可以设计并实现自己的布局类,只需要将自己设计的布局类作为LayoutManager的子类就可以,比如传奇的Borland公司曾在JBuilder中提供了一种新的布局方式——XYLayout,作为对JDK提供的Layout类的补充。对于客户端而言,只需要使用Container类提供的setLayout()方法就可设置任何具体布局方式,无须关心该布局的具体实现。在JDK中,Container类的代码片段如下:
public class Container extends Component {
……
LayoutManager layoutMgr;
……
public void setLayout(LayoutManager mgr) {
layoutMgr = mgr;
……
}
……
}
从上述代码可以看出,Container作为环境类,针对抽象策略类LayoutManager进行编程,用户在使用时,根据“里氏代换原则”,只需要在setLayout()方法中传入一个具体布局对象即可,无须关心它的具体实现。
(2) 除了基于Java语言的应用外,在使用其他面向对象技术开发的软件中,策略模式也得到了广泛的应用。
在微软公司提供的演示项目PetShop 4.0中就使用策略模式来处理同步订单和异步订单的问题。在PetShop 4.0的BLL(Business Logic Layer,业务逻辑层)子项目中有一个OrderAsynchronous类和一个OrderSynchronous类,它们都继承自IOrderStrategy接口,如图:

在图中,OrderSynchronous以一种同步的方式处理订单,而OrderAsynchronous先将订单存放在一个队列中,然后再对队列里的订单进行处理,以一种异步方式对订单进行处理。
BLL的Order类通过反射机制从配置文件中读取策略配置的信息,以决定到底是使用哪种订单处理方式。配置文件web.config中代码片段如下所示:
……
<add key="OrderStrategyClass" value="PetShop.BLL.OrderSynchronous"/>
……
用户只需要修改配置文件即可更改订单处理方式,提高了系统的灵活性。
策略模式用于算法的自由切换和扩展,它是应用较为广泛的设计模式之一。策略模式对应于解决某一问题的一个算法族,允许用户从该算法族中任选一个算法来解决某一问题,同时可以方便地更换算法或者增加新的算法。只要涉及到算法的封装、复用和切换都可以考虑使用策略模式。
策略模式的主要优点如下:
(1) 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
(2) 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,恰当使用继承可以把公共的代码移到抽象策略类中,从而避免重复的代码。
(3) 策略模式提供了一种可以替换继承关系的办法。如果不使用策略模式,那么使用算法的环境类就可能会有一些子类,每一个子类提供一种不同的算法。但是,这样一来算法的使用就和算法本身混在一起,不符合“单一职责原则”,决定使用哪一种算法的逻辑和该算法本身混合在一起,从而不可能再独立演化;而且使用继承无法实现算法或行为在程序运行时的动态切换。
(4) 使用策略模式可以避免多重条件选择语句。多重条件选择语句不易维护,它把采取哪一种算法或行为的逻辑与算法或行为本身的实现逻辑混合在一起,将它们全部硬编码(Hard Coding)在一个庞大的多重条件选择语句中,比直接继承环境类的办法还要原始和落后。
(5) 策略模式提供了一种算法的复用机制,由于将算法单独提取出来封装在策略类中,因此不同的环境类可以方便地复用这些策略类。
策略模式的主要缺点如下:
(1) 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。
(2) 策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类。
(3) 无法同时在客户端使用多个策略类,也就是说,在使用策略模式时,客户端每次只能使用一个策略类,不支持使用一个策略类完成部分功能后再使用另一个策略类来完成剩余功能的情况。
在以下情况下可以考虑使用策略模式:
(1) 一个系统需要动态地在几种算法中选择一种,那么可以将这些算法封装到一个个的具体算法类中,而这些具体算法类都是一个抽象算法类的子类。换言之,这些具体算法类均有统一的接口,根据“里氏代换原则”和面向对象的多态性,客户端可以选择使用任何一个具体算法类,并只需要维持一个数据类型是抽象算法类的对象。
(2) 一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重条件选择语句来实现。此时,使用策略模式,把这些行为转移到相应的具体策略类里面,就可以避免使用难以维护的多重条件选择语句。
(3) 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法与相关的数据结构,可以提高算法的保密性与安全性。
在现实生活中,很多事情都包含几个实现步骤,例如请客吃饭,无论吃什么,一般都包含点单、吃东西、买单等几个步骤,通常情况下这几个步骤的次序是:点单 –> 吃东西 –> 买单。在这三个步骤中,点单和买单大同小异,最大的区别在于第二步——吃什么?吃面条和吃满汉全席可大不相同,如图:

请客吃饭示意图
在软件开发中,有时也会遇到类似的情况,某个方法的实现需要多个步骤(类似“请客”),其中有些步骤是固定的(类似“点单”和“买单”),而有些步骤并不固定,存在可变性(类似“吃东西”)。为了提高代码的复用性和系统的灵活性,可以使用一种称之为模板方法模式的设计模式来对这类情况进行设计,在模板方法模式中,将实现功能的每一个步骤所对应的方法称为基本方法(例如“点单”、“吃东西”和“买单”),而调用这些基本方法同时定义基本方法的执行次序的方法称为模板方法(例如“请客”)。在模板方法模式中,可以将相同的代码放在父类中,例如将模板方法“请客”以及基本方法“点单”和“买单”的实现放在父类中,而对于基本方法“吃东西”,在父类中只做一个声明,将其具体实现放在不同的子类中,在一个子类中提供“吃面条”的实现,而另一个子类提供“吃满汉全席”的实现。通过使用模板方法模式,一方面提高了代码的复用性,另一方面还可以利用面向对象的多态性,在运行时选择一种具体子类,实现完整的“请客”方法,提高系统的灵活性和可扩展性。
模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
Template Method Pattern: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.
模板方法模式是一种基于继承的代码复用技术,它是一种类行为型模式。
模板方法模式是结构最简单的行为型设计模式,在其结构中只存在父类与子类之间的继承关系。
通过使用模板方法模式,可以将一些复杂流程的实现步骤封装在一系列基本方法中,在抽象父类中提供一个称之为模板方法的方法来定义这些基本方法的执行次序,而通过其子类来覆盖某些步骤,从而使得相同的算法框架可以有不同的执行结果。模板方法模式提供了一个模板方法来定义算法框架,而某些具体步骤的实现可以在其子类中完成。
模板方法模式结构比较简单,其核心是抽象类和其中的模板方法的设计

模板方法模式包含如下两个角色:
**(1) AbstractClass(抽象类):**在抽象类中定义了一系列基本操作(PrimitiveOperations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时,在抽象类中实现了一个模板方法(Template Method),用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。
**(2) ConcreteClass(具体子类):**它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。
在实现模板方法模式时,开发抽象类的软件设计师和开发具体子类的软件设计师之间可以进行协作。一个设计师负责给出一个算法的轮廓和框架,另一些设计师则负责给出这个算法的各个逻辑步骤。实现这些具体逻辑步骤的方法即为基本方法,而将这些基本方法汇总起来的方法即为模板方法,模板方法模式的名字也因此而来。下面将详细介绍模板方法和基本方法:
1. 模板方法
一个模板方法是定义在抽象类中的、把基本操作方法组合在一起形成一个总算法或一个总行为的方法。这个模板方法定义在抽象类中,并由子类不加以修改地完全继承下来。模板方法是一个具体方法,它给出了一个顶层逻辑框架,而逻辑的组成步骤在抽象类中可以是具体方法,也可以是抽象方法。由于模板方法是具体方法,因此模板方法模式中的抽象层只能是抽象类,而不是接口。
2. 基本方法
基本方法是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:抽象方法(Abstract Method)、具体方法(Concrete Method)和钩子方法(Hook Method)。
(1) 抽象方法:一个抽象方法由抽象类声明、由其具体子类实现。在C#语言里一个抽象方法以abstract关键字标识。
(2) 具体方法:一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
(3) 钩子方法:一个钩子方法由一个抽象类或具体类声明并实现,而其子类可能会加以扩展。通常在父类中给出的实现是一个空实现(可使用virtual关键字将其定义为虚函数),并以该空实现作为方法的默认实现,当然钩子方法也可以提供一个非空的默认实现。
在模板方法模式中,钩子方法有两类:第一类钩子方法可以与一些具体步骤“挂钩”,以实现在不同条件下执行模板方法中的不同步骤,这类钩子方法的返回类型通常是bool类型的,这类方法名一般为IsXXX(),用于对某个条件进行判断,如果条件满足则执行某一步骤,否则将不执行,如下代码片段所示:
……
//模板方法
public void TemplateMethod()
{
Open();
Display();
//通过钩子方法来确定某步骤是否执行
if (IsPrint())
{
Print();
}
}
//钩子方法
public bool IsPrint()
{
return true;
}
……
在代码中IsPrint()方法即是钩子方法,它可以决定Print()方法是否执行,一般情况下,钩子方法的返回值为true,如果不希望某方法执行,可以在其子类中覆盖钩子方法,将其返回值改为false即可,这种类型的钩子方法可以控制方法的执行,对一个算法进行约束。
还有一类钩子方法就是实现体为空的具体方法,子类可以根据需要覆盖或者继承这些钩子方法,与抽象方法相比,这类钩子方法的好处在于子类如果没有覆盖父类中定义的钩子方法,编译可以正常通过,但是如果没有覆盖父类中声明的抽象方法,编译将报错。
在模板方法模式中,抽象类的典型代码如下:
abstract class AbstractClass
{
//模板方法
public void TemplateMethod()
{
PrimitiveOperation1();
PrimitiveOperation2();
PrimitiveOperation3();
}
//基本方法—具体方法
public void PrimitiveOperation1()
{
//实现代码
}
//基本方法—抽象方法
public abstract void PrimitiveOperation2();
//基本方法—钩子方法
public virtual void PrimitiveOperation3()
{ }
}
在抽象类中,模板方法TemplateMethod()定义了算法的框架,在模板方法中调用基本方法以实现完整的算法,每一个基本方法如PrimitiveOperation1()、PrimitiveOperation2()等均实现了算法的一部分,对于所有子类都相同的基本方法可在父类提供具体实现,例如PrimitiveOperation1(),否则在父类声明为抽象方法或钩子方法,由不同的子类提供不同的实现,例如PrimitiveOperation2()和PrimitiveOperation3()。
可在抽象类的子类中提供抽象步骤的实现,也可覆盖父类中已经实现的具体方法,具体子类的典型代码如下:
class ConcreteClass : AbstractClass
{
public override void PrimitiveOperation2()
{
//实现代码
}
public override void PrimitiveOperation3()
{
//实现代码
}
}
在模板方法模式中,由于面向对象的多态性,子类对象在运行时将覆盖父类对象,子类中定义的方法也将覆盖父类中定义的方法,因此程序在运行时,具体子类的基本方法将覆盖父类中定义的基本方法,子类的钩子方法也将覆盖父类的钩子方法,从而可以通过在子类中实现的钩子方法对父类方法的执行进行约束,实现子类对父类行为的反向控制。
1. 需求描述
某软件公司欲为某银行的业务支撑系统开发一个利息计算模块,利息计算流程如下:
(1) 系统根据账号和密码验证用户信息,如果用户信息错误,系统显示出错提示;
(2) 如果用户信息正确,则根据用户类型的不同使用不同的利息计算公式计算利息(如活期账户和定期账户具有不同的利息计算公式);
(3) 系统显示利息。
试使用模板方法模式设计该利息计算模块。
2. 实例类图
通过分析,本实例结构图如图。

银行利息计算模块结构图
Account充当抽象类角色,CurrentAccount和SavingAccount充当具体子类角色。
3. 实例代码
(1) Account:账户类,充当抽象类。
abstract class Account {
//基本方法--抽象方法
abstract void calculateInterest();
//基本方法--具体方法
public void display() {
System.out.println("显示利息!");
}
//基本方法--具体方法
public boolean validate(String account, String password) {
System.out.println("账号: "+ account);
System.out.println("密码: "+ password);
//模拟登录
return "张无忌".equals(account) && "123456".equals(password);
}
public void handle(String account, String password) {
if (!validate(account, password)) {
System.out.println("账号或密码错误");
return;
}
calculateInterest();
display();
}
}
// CurrentAccount:活期账户类,充当具体子类。
public class CurrentAccount extends Account{
//覆盖父类的抽象基本方法
@Override
void calculateInterest() {
System.out.println("按活期利率计算利息!");
}
}
// SavingAccount:定期账户类,充当具体子类。
public class SavingAccount extends Account{
@Override
void calculateInterest() {
System.out.println("按定期利率计算利息!");
}
}
客户端测试类
public class Client {
public static void main(String[] args) {
Account account;
account = new CurrentAccount();
account.handle("张无忌", "123456");
}
}
输出:
账号: 张无忌
密码: 123456
按活期利率计算利息!
显示利息!
如果需要更换具体子类,无须修改源代码,只需修改配置文件App.config,例如将活期账户(CurrentAccount)改为定期账户(Saving Account),只需将存储在配置文件中的具体子类CurrentAccount改为SavingAccount
public class Client {
public static void main(String[] args) {
Account account;
// account = new CurrentAccount();
account = new SavingAccount();
account.handle("张无忌", "123456");
}
}
重新运行客户端程序,输出结果如下:
账号: 张无忌
密码: 123456
按定期利率计算利息!
显示利息!
如果需要增加新的具体子类(新的账户类型),原有代码均无须修改,完全符合开闭原则。
模板方法模式中,在父类中提供了一个定义算法框架的模板方法,还提供了一系列抽象方法、具体方法和钩子方法,其中钩子方法的引入使得子类可以控制父类的行为。最简单的钩子方法就是空方法,代码如下:
public virtual void Display() { }
当然也可以在钩子方法中定义一个默认的实现,如果子类不覆盖钩子方法,则执行父类的默认实现代码。
另一种钩子方法可以实现对其他方法进行约束,这种钩子方法通常返回一个bool类型,即返回true或false,用来判断是否执行某一个基本方法,下面通过一个实例来说明这种钩子方法的使用。
某软件公司欲为销售管理系统提供一个数据图表显示功能,该功能的实现包括如下几个步骤:
(1) 从数据源获取数据;
(2) 将数据转换为XML格式;
(3) 以某种图表方式显示XML格式的数据。
该功能支持多种数据源和多种图表显示方式,但所有的图表显示操作都基于XML格式的数据,因此可能需要对数据进行转换,如果从数据源获取的数据已经是XML数据则无须转换。
由于该数据图表显示功能的三个步骤次序是固定的,且存在公共代码(例如数据格式转换代码),满足模板方法模式的适用条件,可以使用模板方法模式对其进行设计。因为数据格式的不同,XML数据可以直接显示,而其他格式的数据需要进行转换,因此第(2)步“将数据转换为XML格式”的执行存在不确定性,为了解决这个问题,可以定义一个钩子方法IsNotXMLData()来对数据转换方法进行控制。通过分析,该图表显示功能的基本结构如图:

数据图表显示功能结构图
可以将公共方法和框架代码放在抽象父类中,代码如下:
abstract class DataViewer {
// 抽象方法: 获取数据
public abstract void getData();
// 具体方法: 转换数据
public void convertData() {
System.out.println("将数据转换为XML格式。");
}
//抽象方法:显示数据
public abstract void DisplayData();
//钩子方法:判断是否为XML格式的数据
public boolean IsNotXMLData() {
return true;
}
//模板方法
public void Process() {
getData();
//如果不是XML格式的数据则进行数据转换
if (IsNotXMLData())
{
convertData();
}
DisplayData();
}
}
public class XMLDataViewer extends DataViewer {
@Override
public void getData() {
System.out.println("从XML文件中获取数据。");
}
@Override
public void DisplayData() {
System.out.println("以柱状图显示数据。");
}
@Override
public boolean IsNotXMLData() {
return false;
}
}
在上面的代码中,引入了一个钩子方法IsNotXMLData(),其返回类型为bool类型,在模板方法中通过它来对数据转换方法ConvertData()进行约束,该钩子方法的默认返回值为true,在子类中可以根据实际情况覆盖该方法,其中用于显示XML格式数据的具体子类XMLDataViewer代码如下:
在具体子类XMLDataViewer中覆盖了钩子方法IsNotXMLData(),返回false,表示该数据已为XML格式,无须执行数据转换方法ConvertData(),客户端代码如下:
public class Client {
public static void main(String[] args) {
DataViewer dataViewer = new XMLDataViewer();
dataViewer.Process();
}
}
该程序运行结果如下:
从XML文件中获取数据。
以柱状图显示数据。
模板方法模式是基于继承的代码复用技术,它体现了面向对象的诸多重要思想,是一种使用较为频繁的模式。模板方法模式广泛应用于框架设计中,以确保通过父类来控制处理流程的逻辑顺序(如框架的初始化,测试流程的设置等)。
模板方法模式的主要优点如下:
(1) 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。
(2) 模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用。
(3) 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。
(4) 在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。
模板方法模式的主要缺点如下:
需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,此时,可结合桥接模式来进行设计。
在以下情况下可以考虑使用模板方法模式:
(1) 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即:一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。
(2) 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。
(3) 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
访问者模式是一种较为复杂的行为型设计模式,它包含访问者和被访问元素两个主要组成部分,这些被访问的元素通常具有不同的类型,且不同的访问者可以对它们进行不同的访问操作。例如处方单中的各种药品信息就是被访问的元素,而划价人员和药房工作人员就是访问者。访问者模式使得用户可以在不修改现有系统的情况下扩展系统的功能,为这些不同类型的元素增加新的操作。
在使用访问者模式时,被访问元素通常不是单独存在的,它们存储在一个集合中,这个集合被称为“对象结构”,访问者通过遍历对象结构实现对其中存储的元素的逐个操作。
访问者模式(Visitor Pattern):提供一个作用于某对象结构中的各元素的操作表示,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。

●Vistor(抽象访问者):抽象访问者为对象结构中每一个具体元素类ConcreteElement声明一个访问操作,从这个操作的名称或参数类型可以清楚知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作。
●ConcreteVisitor(具体访问者):具体访问者实现了每个由抽象访问者声明的操作,每一个操作用于访问对象结构中一种类型的元素。
●Element(抽象元素):抽象元素一般是抽象类或者接口,它定义一个accept()方法,该方法通常以一个抽象访问者作为参数。【稍后将介绍为什么要这样设计。】
●ConcreteElement(具体元素):具体元素实现了accept()方法,在accept()方法中调用访问者的访问方法以便完成对一个元素的操作。
● ObjectStructure(对象结构):对象结构是一个元素的集合,它用于存放元素对象,并且提供了遍历其内部元素的方法。它可以结合组合模式来实现,也可以是一个简单的集合对象,如一个List对象或一个Set对象。
访问者模式中对象结构存储了不同类型的元素对象,以供不同访问者访问。访问者模式包括两个层次结构,一个是访问者层次结构,提供了抽象访问者和具体访问者,一个是元素层次结构,提供了抽象元素和具体元素。相同的访问者可以以不同的方式访问不同的元素,相同的元素可以接受不同访问者以不同访问方式访问。在访问者模式中,增加新的访问者无须修改原有系统,系统具有较好的可扩展性。
在访问者模式中,抽象访问者定义了访问元素对象的方法,通常为每一种类型的元素对象都提供一个访问方法,而具体访问者可以实现这些访问方法。这些访问方法的命名一般有两种方式:一种是直接在方法名中标明待访问元素对象的具体类型,如visitElementA(ElementA elementA),还有一种是统一取名为visit(),通过参数类型的不同来定义一系列重载的visit()方法。当然,如果所有的访问者对某一类型的元素的访问操作都相同,则可以将操作代码移到抽象访问者类中,其典型代码如下所示:
abstract class Visitor
{
public abstract void visit(ConcreteElementA elementA);
public abstract void visit(ConcreteElementB elementB);
public void visit(ConcreteElementC elementC)
{
//元素ConcreteElementC操作代码
}
}
在这里使用了重载visit()方法的方式来定义多个方法用于操作不同类型的元素对象。在抽象访问者Visitor类的子类ConcreteVisitor中实现了抽象的访问方法,用于定义对不同类型元素对象的操作,具体访问者类典型代码如下所示:
class ConcreteVisitor extends Visitor
{
public void visit(ConcreteElementA elementA)
{
//元素ConcreteElementA操作代码
}
public void visit(ConcreteElementB elementB)
{
//元素ConcreteElementB操作代码
}
}
对于元素类而言,在其中一般都定义了一个accept()方法,用于接受访问者的访问,典型的抽象元素类代码如下所示:
interface Element
{
public void accept(Visitor visitor);
}
需要注意的是该方法传入了一个抽象访问者Visitor类型的参数,即针对抽象访问者进行编程,而不是具体访问者,在程序运行时再确定具体访问者的类型,并调用具体访问者对象的visit()方法实现对元素对象的操作。在抽象元素类Element的子类中实现了accept()方法,用于接受访问者的访问,在具体元素类中还可以定义不同类型的元素所特有的业务方法,其典型代码如下所示:
class ConcreteElementA implements Element
{
public void accept(Visitor visitor)
{
visitor.visit(this);
}
public void operationA()
{
//业务方法
}
}
在具体元素类ConcreteElementA的accept()方法中,通过调用Visitor类的visit()方法实现对元素的访问,并以当前对象作为visit()方法的参数。其具体执行过程如下:
(1) 调用具体元素类的accept(Visitor visitor)方法,并将Visitor子类对象作为其参数;
(2) 在具体元素类accept(Visitor visitor)方法内部调用传入的Visitor对象的visit()方法,如visit(ConcreteElementA elementA),将当前具体元素类对象(this)作为参数,如visitor.visit(this);
(3) 执行Visitor对象的visit()方法,在其中还可以调用具体元素对象的业务方法。
这种调用机制也称为“双重分派”,正因为使用了双重分派机制,使得增加新的访问者无须修改现有类库代码,只需将新的访问者对象作为参数传入具体元素对象的accept()方法,程序运行时将回调在新增Visitor类中定义的visit()方法,从而增加新的元素访问方式。
思考双重分派机制如何用代码实现?
在访问者模式中,对象结构是一个集合,它用于存储元素对象并接受访问者的访问,其典型代码如下所示:
class ObjectStructure
{
private ArrayList<Element> list = new ArrayList<Element>(); //定义一个集合用于存储元素对象
public void accept(Visitor visitor)
{
Iterator i=list.iterator();
while(i.hasNext())
{
((Element)i.next()).accept(visitor); //遍历访问集合中的每一个元素
}
}
public void addElement(Element element)
{
list.add(element);
}
public void removeElement(Element element)
{
list.remove(element);
}
}
在对象结构中可以使用迭代器对存储在集合中的元素对象进行遍历,并逐个调用每一个对象的accept()方法,实现对元素对象的访问操作。
Sunny软件公司欲为某银行开发一套OA系统,在该OA系统中包含一个员工信息管理子系统,该银行员工包括正式员工和临时工,每周人力资源部和财务部等部门需要对员工数据进行汇总,汇总数据包括员工工作时间、员工工资等。该公司基本制度如下:
(1) 正式员工(Full time Employee)每周工作时间为40小时,不同级别、不同部门的员工每周基本工资不同;如果超过40小时,超出部分按照100元/小时作为加班费;如果少于40小时,所缺时间按照请假处理,请假所扣工资以80元/小时计算,直到基本工资扣除到零为止。除了记录实际工作时间外,人力资源部需记录加班时长或请假时长,作为员工平时表现的一项依据。
(2) 临时工(Part time Employee)每周工作时间不固定,基本工资按小时计算,不同岗位的临时工小时工资不同。人力资源部只需记录实际工作时间。
人力资源部和财务部工作人员可以根据各自的需要对员工数据进行汇总处理,人力资源部负责汇总每周员工工作时间,而财务部负责计算每周员工工资。
Sunny软件公司开发人员针对上述需求,提出了一个初始解决方案,其核心代码如下所示:
import java.util.*;
package org.example.demo03;
class EmployeeList {
private ArrayList<Employee> list = new ArrayList<Employee>(); //员工集合
//增加员工
public void addEmployee(Employee employee) {
list.add(employee);
}
//处理员工数据
public void handle(String departmentName) {
if(departmentName.equalsIgnoreCase("财务部")) //财务部处理员工数据{
for(Object obj : list) {
if(obj.getClass().getName().equalsIgnoreCase("FulltimeEmployee")) {
System.out.println("财务部处理全职员工数据!");
} else {
System.out.println("财务部处理兼职员工数据!");
}
}
}else if(departmentName.equalsIgnoreCase("人力资源部")) //人力资源部处理员工数据{
for(Object obj : list){
if(obj.getClass().getName().equalsIgnoreCase("FulltimeEmployee")) {
System.out.println("人力资源部处理全职员工数据!");
} else {
System.out.println("人力资源部处理兼职员工数据!");
}
}
}
}
}
在EmployeeList类的handle()方法中,通过对部门名称和员工类型进行判断,不同部门对不同类型的员工进行了不同的处理,满足了员工数据汇总的要求。但是该解决方案存在如下几个问题:
(1) EmployeeList类非常庞大,它将各个部门处理各类员工数据的代码集中在一个类中,在具体实现时,代码将相当冗长,EmployeeList类承担了过多的职责,既不方便代码的复用,也不利于系统的扩展,违背了“单一职责原则”。
(2)在代码中包含大量的“if…else…”条件判断语句,既需要对不同部门进行判断,又需要对不同类型的员工进行判断,还将出现嵌套的条件判断语句,导致测试和维护难度增大。
(3)如果要增加一个新的部门来操作员工集合,不得不修改EmployeeList类的源代码,在handle()方法中增加一个新的条件判断语句和一些业务处理代码来实现新部门的访问操作。这违背了“开闭原则”,系统的灵活性和可扩展性有待提高。
(4)如果要增加一种新类型的员工,同样需要修改EmployeeList类的源代码,在不同部门的处理代码中增加对新类型员工的处理逻辑,这也违背了“开闭原则”。
如何解决上述问题?如何为同一集合对象中的元素提供多种不同的操作方式?访问者模式就是一个值得考虑的解决方案,它可以在一定程度上解决上述问题(解决大部分问题)。访问者模式可以为为不同类型的元素提供多种访问操作方式,而且可以在不修改原有系统的情况下增加新的操作方式。
Sunny软件公司开发人员使用访问者模式对OA系统中员工数据汇总模块进行重构,使得系统可以很方便地增加新类型的访问者,更加符合“单一职责原则”和“开闭原则”,重构后的基本结构如图26-3所示:

在图26-3中,FADepartment表示财务部,HRDepartment表示人力资源部,它们充当具体访问者角色,其抽象父类Department充当抽象访问者角色;EmployeeList充当对象结构,用于存储员工列表;FulltimeEmployee表示正式员工,ParttimeEmployee表示临时工,它们充当具体元素角色,其父接口Employee充当抽象元素角色。完整代码如下所示:
//员工类:抽象元素类
interface Employee{
void accept(Department handler); //接受一个抽象访问者访问
}
import lombok.AllArgsConstructor;
import lombok.Data;
//全职员工类:具体元素类
@Data
@AllArgsConstructor
class FulltimeEmployee implements Employee{
private String name;
private double weeklyWage;
private int workTime;
public void accept(Department handler) {
handler.visit(this); //调用访问者的访问方法
}
}
import lombok.AllArgsConstructor;
import lombok.Data;
//兼职员工类:具体元素类
@Data
@AllArgsConstructor
class ParttimeEmployee implements Employee{
private String name;
private double hourWage;
private int workTime;
public void accept(Department handler) {
handler.visit(this); //调用访问者的访问方法
}
}
//部门类:抽象访问者类
abstract class Department{
//声明一组重载的访问方法,用于访问不同类型的具体元素
public abstract void visit(FulltimeEmployee employee);
public abstract void visit(ParttimeEmployee employee);
}
//财务部类:具体访问者类
class FADepartment extends Department{
//实现财务部对全职员工的访问
public void visit(FulltimeEmployee employee) {
int workTime = employee.getWorkTime();
double weekWage = employee.getWeeklyWage();
if(workTime > 40) {
weekWage = weekWage + (workTime - 40) * 100;
}
else if(workTime < 40) {
weekWage = weekWage - (40 - workTime) * 80;
if(weekWage < 0) {
weekWage = 0;
}
}
System.out.println("正式员工" + employee.getName() + "实际工资为:" + weekWage + "元。");
}
//实现财务部对兼职员工的访问
public void visit(ParttimeEmployee employee) {
int workTime = employee.getWorkTime();
double hourWage = employee.getHourWage();
System.out.println("临时工" + employee.getName() + "实际工资为:" + workTime * hourWage + "元。");
}
}
//人力资源部类:具体访问者类
class HRDepartment extends Department{
//实现人力资源部对全职员工的访问
public void visit(FulltimeEmployee employee) {
int workTime = employee.getWorkTime();
System.out.println("正式员工" + employee.getName() + "实际工作时间为:" + workTime + "小时。");
if(workTime > 40) {
System.out.println("正式员工" + employee.getName() + "加班时间为:" + (workTime - 40) + "小时。");
}
else if(workTime < 40) {
System.out.println("正式员工" + employee.getName() + "请假时间为:" + (40 - workTime) + "小时。");
}
}
//实现人力资源部对兼职员工的访问
public void visit(ParttimeEmployee employee) {
int workTime = employee.getWorkTime();
System.out.println("临时工" + employee.getName() + "实际工作时间为:" + workTime + "小时。");
}
}
import java.util.ArrayList;
//员工列表类:对象结构
class EmployeeList{
//定义一个集合用于存储员工对象
private final ArrayList<Employee> list = new ArrayList<Employee>();
public void addEmployee(Employee employee) {
list.add(employee);
}
//遍历访问员工集合中的每一个员工对象
public void accept(Department handler) {
for(Employee obj : list) {
obj.accept(handler);
}
}
}
编写如下客户端测试代码:
class Client {
public static void main(String[] args) {
EmployeeList list = new EmployeeList();
Employee fte1,fte2,fte3,pte1,pte2;
fte1 = new FulltimeEmployee("张无忌",3200.00,45);
fte2 = new FulltimeEmployee("杨过",2000.00,40);
fte3 = new FulltimeEmployee("段誉",2400.00,38);
pte1 = new ParttimeEmployee("洪七公",80.00,20);
pte2 = new ParttimeEmployee("郭靖",60.00,18);
list.addEmployee(fte1);
list.addEmployee(fte2);
list.addEmployee(fte3);
list.addEmployee(pte1);
list.addEmployee(pte2);
Department dep;
dep = new FADepartment();
// dep = new HRDepartment();
list.accept(dep);
}
}
编译并运行程序,输出结果如下:
正式员工张无忌实际工资为:3700.0元。
正式员工杨过实际工资为:2000.0元。
正式员工段誉实际工资为:2240.0元。
临时工洪七公实际工资为:1600.0元。
临时工郭靖实际工资为:1080.0元。
如果需要更换具体访问者类,无须修改源代码,只需修改配置文件,例如将访问者类由财务部改为人力资源部,只需将存储在配置文件中的具体访问者类FADepartment改为HRDepartment,如下代码所示:
class Client {
public static void main(String[] args) {
EmployeeList list = new EmployeeList();
Employee fte1,fte2,fte3,pte1,pte2;
fte1 = new FulltimeEmployee("张无忌",3200.00,45);
fte2 = new FulltimeEmployee("杨过",2000.00,40);
fte3 = new FulltimeEmployee("段誉",2400.00,38);
pte1 = new ParttimeEmployee("洪七公",80.00,20);
pte2 = new ParttimeEmployee("郭靖",60.00,18);
list.addEmployee(fte1);
list.addEmployee(fte2);
list.addEmployee(fte3);
list.addEmployee(pte1);
list.addEmployee(pte2);
Department dep;
// dep = new FADepartment();
dep = new HRDepartment();
list.accept(dep);
}
}
重新运行客户端程序,输出结果如下:
正式员工张无忌实际工作时间为:45小时。
正式员工张无忌加班时间为:5小时。
正式员工杨过实际工作时间为:40小时。
正式员工段誉实际工作时间为:38小时。
正式员工段誉请假时间为:2小时。
临时工洪七公实际工作时间为:20小时。
临时工郭靖实际工作时间为:18小时。
如果要在系统中增加一种新的访问者,无须修改源代码,只要增加一个新的具体访问者类即可,在该具体访问者中封装了新的操作元素对象的方法。从增加新的访问者的角度来看,访问者模式符合“开闭原则”。
如果要在系统中增加一种新的具体元素,例如增加一种新的员工类型为“退休人员”,由于原有系统并未提供相应的访问接口(在抽象访问者中没有声明任何访问“退休人员”的方法),因此必须对原有系统进行修改,在原有的抽象访问者类和具体访问者类中增加相应的访问方法。从增加新的元素的角度来看,访问者模式违背了“开闭原则”。
综上所述,访问者模式与抽象工厂模式类似,对“开闭原则”的支持具有倾斜性,可以很方便地添加新的访问者,但是添加新的元素较为麻烦。
在访问者模式中,包含一个用于存储元素对象集合的对象结构,我们通常可以使用迭代器来遍历对象结构,同时具体元素之间可以存在整体与部分关系,有些元素作为容器对象,有些元素作为成员对象,可以使用组合模式来组织元素。引入组合模式后的访问者模式结构图如图所示:

需要注意的是,在图所示结构中,由于叶子元素的遍历操作已经在容器元素中完成,因此要防止单独将已增加到容器元素中的叶子元素再次加入对象结构中,对象结构中只保存容器元素和孤立的叶子元素。
由于访问者模式的使用条件较为苛刻,本身结构也较为复杂,因此在实际应用中使用频率不是特别高。当系统中存在一个较为复杂的对象结构,且不同访问者对其所采取的操作也不相同时,可以考虑使用访问者模式进行设计。在XML文档解析、编译器的设计、复杂集合对象的处理等领域访问者模式得到了一定的应用。
(1) 增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合“开闭原则”。
(2) 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散在一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问。
(3) 让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作。
(1) 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”的要求。
(2) 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。
(1) 一个对象结构包含多个类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
(2) 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
(3) 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
关于编程语言模块的一些介绍
find . -name "*.html" -exec sed -i '/window.location.href/d' {} \;
Git支持多种协议,包括https,但通过ssh支持的原生git协议速度最快。 使用https除了速度慢以外,还有个最大的麻烦是每次推送都必须输入口令,但是在某些只开放http端口的公司内部就无法使用ssh协议而只能用https。
创建分支并切换: git checkout -b 分支名称
等同于下面两句话
(
1.创建分支: git branch 分支名称
2.切换到某分支: git checkout 分支名称
)
本地创建分支并关联远程分支: git checkout -b 本地分支名 origin/远程分支名
建立本地分支和远程分支的关联,使用: git branch --set-upstream 本地分支名 origin/远程分支名
合并指定分支到当前分支: git merge 目标分支名
查看所有本地分支(当前分支前会有一个*号): git branch
删除某个分支: git branch -d 分支名
强行删除分支: git branch -D 分支名
用带参数的git log也可以看到分支的合并情况: git log --graph --pretty=oneline --abbrev-commit (用git log --graph命令可以看到分支合并图)
带参数的合并,并产生一个新的commit: git merge --no-ff -m "merge with no-ff" dev
存储当前工作空间,解决其他问题,并还原
1. git stash "贮藏描述" (存储当前工作区)
2. 其他操作,例如修复bug要创建新的分支,提交合并并且删除bug分支
3. git stash list (查看工作区)
(会输出)
stash@{0}: WIP on master: 2b5faea new info text
stash@{1}: WIP on dev: b0f1f6a conflict fixed
stash@{2}: WIP on dev: b0f1f6a conflict fixed
4. git shash apply stash@{n} (恢复到哪个工作区)
5. git shash drop(删除缓冲区序号最小的工作区)
(6. 45两步可以用 git stash pop 会直接还原到序号最小的工作区并将其删除)
拉取: git pull 如果有冲突,要先处理冲突。
标签 Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。 打一个新的标签: git tag 标签名 给某一个历史提交打标签: git tag 标签名 提交Hash
查看标签列表: git tag
查看具体标签: git show tagName
创建带有说明的标签,用-a指定标签名,-m指定说明文字: git tag -a v0.1 -m "标签说明内容" 提交Hash
通过-s用私钥签名一个标签: git tag -s v0.2 -m "标签说明内容" 提交Hash (签名采用PGP签名,因此,必须首先安装gpg(GnuPG),如果没有找到gpg,或者没有gpg密钥对,就会报错)
删除本地标签: git tag -d tagName
推送标签: git push origin v1.0
推送所有标签: git push origin --tags
删除服务器上的标签:
1.先删除本地标签
2. git push origin :refs/tags/tagName
一些基本的操作 指定你的信息 - git config –global user.name “Your Name” - git config –global user.email “email@example.com” 查看信息 - git config –global user.name - git config –global user.email 创建管理仓库: git init
把文件添加到仓库: git add path/文件1 path/文件2 ...
交互式添加: git add -i
查看提交日志: git log (--pretty=oneline)
查看最后一次提交: git log -1
提交
- git commit -m "提交日志记录"
- git commit -a 可以不用写提交记录,但是强烈不建议这么做
查看哪个文件被改动过: git status
查看具体文件具体怎么改动的
- git diff //查看当前所有的更改
- git diff path/文件 //查看某个文件的改动
- git diff --cached // 查看将要提交哪些文件去commit
- git diff master..test //查询master和test两个分支的差异
- git diff master...test //查询 master和test两个分支的共有父分支 和 test分支的差异
- git diff -- 目录 // (-- 目录 一定要连着, 尽量写在最后), 查询某个目录下的差异
- git diff --stat //简要的差异, 不输出详细差异内容, 只看看改了那些文件
回溯到上个版本(上上个版本|某个版本): git reset --hard HEAD^(HEAD^^|commit id)
如果你后悔了一个推送,撤销远程分支的提交: git reset --hard origin/远程分支 //这个操作很危险, 不得已不要做
还原更改
让这个文件回到最近一次git commit或git add时的状态
git checkout -- file (命令中的--很重要,没有--,就变成了“切换到另一个分支”的命令)
清除最近一次的add操作(添加到缓存区操作): git reset HEAD file
让Git显示颜色,会让命令输出看起来更醒目: git config --global color.ui true
忽略特殊文件
1. 创建 .gitignore 文件
2. 如果要忽略python 那么写 *.py *.py[cod]
配置别名
--global是针对当前用户起作用的,如果不加,那只针对当前的仓库起作用,全局git的配置文件是用户主目录下的 .gitconfig文件
- git config --global alias.st status
- git config --global alias.unstage 'reset HEAD'
- git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
查看命令历史: git reflog
于服务器的交互操作 关联一个远程库 使用ssh协议添加库,这个需要添加ssh_key 来交互认证不用用户名,自己建服务器都是用这种方式 git remote add origin git@server-name:path/repo-name.git 使用https添加库,每次推送代码都会要求写用户名密码来认证,在github上可以用,自己的服务器不清楚 git remote add origin https://path/proname.git 推送到远程仓库 git push -u origin 分支名 第一次推送master分支的所有内容 git push origin 分支名 推送最新修改 git push -f origin 分支名 //强制推送–慎用 git push origin +分支名 //在push之前拉取内容 克隆远程仓库 git clone git@server-name:path/repo-name.git //github用法 git clone https://path/proname.git 删除远程分支 git push origin :dev(这个空格很重要)
其他操作 桌面平台显示项目提交记录:gitk //执行了gitk后会有一个很漂亮的图形的显示项目的历史,windows桌面环境有 git clone 远程地址(本地其他仓库,只要是有.git) 本地目录
辅助操作 创建ssh_key ssh-keygen -t rsa -C “ssh-keygen@163.com” 在用户主目录会生成 .ssh目录,里面有id_rsa和id_rsa.pub两个文件
细节即是架构
下面是原文摘录,我有类似观点,但是原文就写得很好,直接摘录。
一直以来,设计(Design)和架构(Architecture)这两个概念让大多数人十分迷惑–什么是设计?什么是架构?二者究竟有什么区别?二者没有区别。一丁点区别都没有!“架构"这个词往往适用于"高层级"的讨论中,这类讨论一般都把"底层"的实现细节排除在外。而"设计"一词,往往用来指代具体的系统底层组织结构和实现的细节。但是,从一个真正的系统架构师的日常工作来看,这些区分是根本不成立的。以给我设计新房子的建筑设计师要做的事情为例。新房子当然是存在着既定架构的,但这个架构具体包含哪些内容呢?首先,它应该包括房屋的形状、外观设计、垂直高度、房间的布局,等等。
但是,如果查看建筑设计师使用的图纸,会发现其中也充斥着大量的设计细节。譬如,我们可以看到每个插座、开关以及每个电灯具体的安装位置,同时也可以看到某个开关与所控制的电灯的具体连接信息;我们也能看到壁炉的具体位置,热水器的大小和位置信息,甚至是污水泵的位置;同时也可以看到关于墙体、屋顶和地基所有非常详细的建造说明。总的来说,架构图里实际上包含了所有的底层设计细节,这些细节信息共同支撑了顶层的架构设计,底层设计信息和顶层架构设计共同组成了整个房屋的架构文档。
软件设计也是如此。底层设计细节和高层架构信息是不可分割的。他们组合在一起,共同定义了整个软件系统,缺一不可。所谓的底层和高层本身就是一系列决策组成的连续体,并没有清晰的分界线。
我们编写、review 细节代码,就是在做架构设计的一部分。我们编写的细节代码构成了整个系统。我们就应该在细节 review 中,始终带着所有架构原则去审视。你会发现,你已经写下了无数让整体变得丑陋的细节,它们背后,都有前人总结过的架构原则。
把代码和文档绑在一起(自解释原则)
写文档是个好习惯。但是写一个别人需要咨询老开发者才能找到的文档,是个坏习惯。这个坏习惯甚至会给工程师们带来伤害。比如,当初始开发者写的文档在一个犄角旮旯(在 wiki 里,但是阅读代码的时候没有在明显的位置看到链接),后续代码被修改了,文档已经过时,有人再找出文档来获取到过时、错误的知识的时候,阅读文档这个同学的开发效率必然受到伤害。所以,如同 Golang 的 godoc 工具能把代码里“按规范来”的注释自动生成一个文档页面一样,我们应该:
▶︎ 按照 godoc 的要求好好写代码的注释。
▶︎ 代码首先要自解释,当解释不了的时候,需要就近、合理地写注释。
▶︎ 当小段的注释不能解释清楚的时候,应该有 doc.go 来解释,或者在同级目录的 ReadMe.md 里注释讲解。
▶︎ 文档需要强大的富文本编辑能力,Down 无法满足,可以写到 wiki 里,同时必须把 wiki 的简单描述和链接放在代码里合适的位置。让阅读和维护代码的同学一眼就看到,能做到及时的维护。
以上总结起来就是,解释信息必须离被解释的东西越近越好。代码能做到自解释,是最棒的。

ETC 价值观(easy to change)
ETC 是一种价值观念,不是一条原则。价值观念是帮助你做决定的: 我应该做这个,还是做那个?当你在软件领域思考时,ETC 是个向导,它能帮助你在不同的路线中选出一条。就像其他一些价值观念一样,你应该让它漂浮在意识思维之下,让它微妙地将你推向正确的方向。
敏捷软件工程,所谓敏捷,就是要能快速变更,并且在变更中保持代码的质量。所以,持有 ETC 价值观看待代码细节、技术方案,我们将能更好地编写出适合敏捷项目的代码。这是一个大的价值观,不是一个基础微观的原则,所以没有例子。本文提到的所有原则,或直接,或间接,都要为 ETC 服务。
DRY 原则(don not repeat yourself)
我认为 DRY 原则是编码原则中最重要的编码原则,没有之一(ETC 是个观念)。不要重复!不要重复!不要重复!

正交性原则(全局变量的危害)
“正交性”是几何学中的术语。我们的代码应该消除不相关事物之间的影响。这是一个简单的道理。我们写代码要“高内聚、低耦合”,这是大家都在提的。
但是,你有为了使用某个 class 一堆能力中的某个能力而去派生它么?你有写过一个 helper 工具,它什么都做么?在腾讯,我相信你是做过的。你自己说,你这是不是为了复用一点点代码,而让两大块甚至多块代码耦合在一起,不再正交了?大家可能并不是不明白正交性的价值,只是不知道怎么去正交。手段有很多,但是首先我就要批判一下 OOP。它的核心是多态,多态需要通过派生/继承来实现。继承树一旦写出来,就变得很难 change,你不得不为了使用一小段代码而去做继承,让代码耦合。
你应该多使用组合,而不是继承。以及,应该多使用 DIP(Dependence Inversion Principle),依赖倒置原则。换个说法,就是面向 interface 编程,面向契约编程,面向切面编程,他们都是 DIP 的一种衍生。写 Golang 的同学就更不陌生了,我们要把一个 struct 作为一个 interface 来使用,不需要显式 implement/extend,仅仅需要持有对应 interface 定义了的函数。这种 duck interface 的做法,让 DIP 来得更简单。AB 两个模块可以独立编码,他们仅仅需要一个依赖 interface 签名,一个刚好实现该 interface 签名。并不需要显式知道对方 interface 签名的两个模块就可以在需要的模块、场景下被组合起来使用。代码在需要被组合使用的时候才产生了一点关系,同时,它们依然保持着独立。
说个正交性的典型案例。全局变量是不正交的!没有充分的理由,禁止使用全局变量。全局变量让依赖了该全局变量的代码段互相耦合,不再正交。特别是一个 pkg 提供一个全局变量给其他模块修改,这个做法会让 pkg 之间的耦合变得复杂、隐秘、难以定位。

单例就是全局变量
后面有“共享状态就是不正确的状态”原则,会进一步讲到。我先给出解决方案,可以通过管道、消息机制来替代共享状态/使用全局变量/使用单例。仅仅能获取此刻最新的状态,通过消息变更状态。要拿到最新的状态,需要重新获取。在必要的时候,引入锁机制。
可逆性原则
可逆性原则是很少被提及的一个原则。可逆性,就是你做出的判断,最好都是可以被逆转的。再换一个容易懂的说法,你最好尽量少认为什么东西是一定的、不变的。比如,你认为你的系统永远服务于用 32 位无符号整数(比如 QQ 号)作为用户标识的系统。你认为你的持久化存储就选型 SQL 存储了。当这些一开始你认为一定的东西,被推翻的时候,你的代码却很难去 change,那么,你的代码就是可逆性做得很差。书里有一个例证,我觉得很好,直接引用过来。
写下这段文字的时间是2019年。自世纪之交以来,我们看到了以下“服务端架构的最佳实践”:
* 大铁块
* 大铁块的组合
* 带负载均衡的商用硬件集群大
* 将程序运行在云虚拟机中大
* 把服务运行在云虚拟机中
* 把云虚拟机换成容器再来一遍
* 基于云的无服务器架构大
* 最后,无可避免的,有些任务又回到了大铁块
…(省略了文字)… 你能为这种架构的变化提前准备吗? 做不到。你能做的就是让修改更容易一点。将第三方 API 隐藏在自己的抽象层之后。将代码分解成多个组件:即使最终会把它们部署到单个大型服务器上,这种方法也比一开始做成庞然大物,然后再切分要容易得多。
与其认为决定是被刻在石头上的,还不如把它们想像成写在沙滩的沙子上。一个大浪随时都可能袭来,卷走一切。腾讯也确实在 20 年内经历了“大铁块”到“云虚拟机换成容器”的几个阶段。几次变化都是伤筋动骨,消耗大量的时间,甚至总会有一些上一个时代残留的服务。就机器数量而论,还不小,一到裁撤季,就很难受。就最近,我看到某个 trpc 插件,直接从环境变量里读取本机 IP,仅仅因为 STKE(Tencent Kubernetes Engine)提供了这个能力。这个细节设计就是不可逆的,将来会有人为它买单,可能价格还不便宜。
依赖倒置原则(DIP)
DIP 原则太重要了,我这里单独列一节来讲解。依赖倒置原则,全称是 Dependence Inversion Principle,简称 DIP。考虑下面这几段代码:
package dippackage
diptype Botton interface {
TurnOn() TurnOff()
}
type UI struct {
botton Botton
}
func NewUI(b Botton) *UI {
return &UI{botton: b}}
func (u *UI) Poll() {
u.botton.TurnOn()
u.botton.TurnOff()
u.botton.TurnOn()
}
package javaimpl
import "fmt"
type Lamp struct {}
func NewLamp() *Lamp {
return &Lamp{}}
func (*Lamp) TurnOn() {
fmt.Println("turn on java lamp")}
func (*Lamp) TurnOff() {
fmt.Println("turn off java lamp")}
package pythonimpl
import "fmt"
type Lamp struct {}
func NewLamp() *Lamp { return &Lamp{}}
func (*Lamp) TurnOn() { fmt.Println("turn on python lamp")}
func (*Lamp) TurnOff() { fmt.Println("turn off python lamp")}
package main
import ( "javaimpl" "pythonimpl" "dip")
func runPoll(b dip.Botton) { ui := NewUI(b) ui.Poll()}
func main() { runPoll(pythonimpl.NewLamp()) runPoll(javaimpl.NewLamp())}
看代码,main pkg 里的 runPoll 函数仅仅面向 Botton interface 编码,main pkg 不再关心 Botton interface 里定义的 TurnOn、TurnOff 的实现细节。实现了解耦。这里,我们能看到 struct UI 需要被注入(inject)一个 Botton interface 才能逻辑完整。所以,DIP 经常换一个名字出现,叫做依赖注入(Dependency Injection)。

从这个依赖图观察。我们发现,一般来说,UI struct 的实现是要应该依赖于具体的 PythonLamp、JavaLamp、其他各种 Lamp,才能让自己的逻辑完整。那就是 UI struct 依赖于各种 Lamp 的实现,才能逻辑完整。但是,我们看上面的代码,却是反过来了。PythonLamp、JavaLamp、其他各种 Lamp 是依赖 Botton interface 的定义,才能用来和 UI struct 组合起来拼接成完整的业务逻辑。变成了,Lamp 的实现细节,依赖于 UI struct 对于 Botton interface 的定义。这个时候,你发现,这种依赖关系被倒置了!依赖倒置原则里的“倒置”,就是这么来的。在 Golang 里,‘PythonLamp、JavaLamp、其他各种 Lamp 是依赖 Botton interface 的定义’,这个依赖是隐性的,没有显式的 implement 和 extend 关键字。代码层面,pkg dip 和 pkg pythonimpl、javaimpl 没有任何依赖关系。他们仅仅需要被你在 main pkg 里组合起来使用。
在 J2EE 里,用户的业务逻辑不再依赖低具体低层的各种存储细节,而仅仅依赖一套配置化的 Java Bean 接口。Object 落地存储的具体细节,被做成了 Java Bean 配置,注入到框架里。这就是 J2EE 的核心科技,并不复杂,其实也没有多么“高不可攀”。在“动态代码”优于“配置”的今天,这种通过配置实现的依赖注入,反而有点过时了。
将知识用纯文本来保存
这也是一个生僻的原则。指代码操作的数据和方案设计文稿,如果没有充分的必要使用特定的方案,就应该使用人类可读的文本来保存、交互。对于方案设计文稿,你能不使用 office 格式,就不使用(office 能极大提升效率才用),最好是原始 text。这是《Unix 编程艺术》也提到了的 Unix 系产生的设计信条。简而言之一句话,当需要确保有一个所有各方都能使用的公共标准,才能实现交互沟通时,纯文本就是这个标准。它是一个接受度最高的通行标准,如果没有必要的理由,我们就应该使用纯文本。
契约式设计
如果你对契约式设计(Design by Contract, DBC)还很陌生,我相信,你和其他端的同学(web、client、后端)联调需求应该是一件很花费时间的事情。你自己编写接口自动化,也会是一件很耗费精力的事情。你先看看它的 wiki 解释吧。grpc + grpc-gateway + swagger 是个很香的东西。
代码是否不多不少刚好完成它宣称要做的事情,可以使用契约加以校验和文档化。TDD 就是全程在不断调整和履行着契约。TDD(Test-Driven Development)是自底向上的编码过程,其实会耗费大量的精力,并且对于一个良好的层级架构没有帮助。TDD 不是强推的规范,但是同学们可以用一用,感受一下。TDD 方法论实现的接口、函数,自我解释能力一般来说比较强,因为它就是一个实现契约的过程。
抛开 TDD 不谈。我们的函数、API,你能快速抓住它描述的核心契约么?它的契约简单么?如果不能、不简单,那你应该要求被 review 的代码做出调整。如果你在指导一个后辈,你应该帮他思考一下,给出至少一个可能的简化、拆解方向。
尽早崩溃
Erlang 发明者、《Erlang 程序设计》作者乔·阿姆斯特朗有一句反复被引用的话:“防御式编程是在浪费时间,让它崩溃。”
尽早崩溃不是说不容错,而是程序应该被设计成允许出故障,有适当的故障监管程序和代码,及时告警,告知工程师,哪里出问题了,而不是尝试掩盖问题,不让程序员知道。当最后程序员知道程序出故障的时候,已经找不到问题出现在哪里了。
特别是一些 recover 之后什么都不做的代码,这种代码简直是毒瘤!当然,崩溃,可以是早一些向上传递 error,不一定就是 panic。同时,我要求大家不要在没有充分的必要性的时候 panic,应该更多地使用向上传递 error,做好 metrics 监控。合格的 Golang 程序员,都不会在没有必要的时候无视 error,会妥善地做好 error 处理、向上传递、监控。一个死掉的程序,通常比一个瘫痪的程序,造成的损害要小得多。
崩溃但是不告警,或者没有补救的办法,不可取。尽早崩溃的题外话是,要在问题出现的时候做合理的告警,有预案,不能掩盖,不能没有预案:

解耦代码让改变容易
这个原则,显而易见,大家自己也常常提,其他原则或多或少都和它有关系。但是我也再提一提。我主要是描述一下它的症状,让同学们更好地警示自己“我这两块代码是不是耦合太重,需要额外引入解耦的设计了”。症状如下:
▶︎ 不相关的 pkg 之间古怪的依赖关系;
▶︎ 对一个模块进行的“简单”修改,会传播到系统中不相关的模块里,或是破坏了系统中的其他部分;
▶︎ 开发人员害怕修改代码,因为他们不确定会造成什么影响;
▶︎ 会议要求每个人都必须参加,因为没有人能确定谁会受到变化的影响。
只管命令不要询问
看看如下三段代码:
func applyDiscount(customer Customer, orderID string, discount float32) {
customer.Orders.Find(orderID).GetTotals().ApplyDiscount(discount)
}
func applyDiscount(customer Customer, orderID string, discount float32) {
customer.FindOrder(orderID).GetTotals().ApplyDiscount(discount)
}
func applyDiscount(customer Customer, orderID string, discount float32) {
customer.FindOrder(orderID).ApplyDiscount(discount)
}
明显,最后一段代码最简洁。不关心 Orders 成员、总价的存在,直接命令 customer 找到 Order 并对其进行打折。当我们调整 Orders 成员、GetTotals()方法的时候,这段代码不用修改。还有一种更吓人的写法:
func applyDiscount(customer Customer, orderID string, discount float32) {
total := customer
.FindOrder(orderID)
.GetTotals()customer
.FindOrder(orderID)
.SetTotal(total*discount)}
它做了更多的查询,关心了更多的细节,变得更加 hard to change 了。我相信大家,特别是客户端同学,写过不少类似的代码。
最好的那一段代码,就是只管给每个 struct 发送命令,要求大家做事儿。怎么做,就内聚在和 struct 关联的方法里,其他人不要去操心。一旦其他人操心了,当需要做修改的时候,就要操心了这个细节的人都一起参与进修改过程。
不要链式调用方法
看下面的例子:
func amount(customer Customer) float32 {
return customer.Orders.Last().Totals().Amount
}
func amount(totals Totals) float32 {
return totals.Amount
}
第二个例子明显优于第一个,它变得更简单、通用、ETC。我们应该给函数传入它关心的最小集合作为参数。而不是我有一个 struct,当某个函数需要这个 struct 的成员的时候,我们把整个 struct 都作为参数传递进去。应该仅仅传递函数关心的最小集合。传进去的一整条调用链对函数来说,都是无关的耦合,只会让代码更 hard to change,让工程师惧怕去修改。这一条原则,和上一条关系很紧密,问题常常同时出现。同样,特别容易出现在客户端代码里。
继承税(多用组合)
继承就是耦合。不仅子类耦合到父类,以及父类的父类等,而且使用子类的代码也耦合到所有祖先类。有些人认为继承是定义新类型的一种方式。他们喜欢设计图表,会展示出类的层次结构。他们看待问题的方式,与维多利亚时代的绅士科学家们看待自然的方式是一样的,即将自然视为须分解到不同类别的综合体。不幸的是,这些图表很快就会为了表示类之间的细微差别而逐层添加,最终可怕地爬满墙壁。由此增加的复杂性,可能使应用程序更加脆弱,因为变更可能在许多层次之间上下波动。因为一些值得商榷的词义消歧方面的原因,C++在20世纪90年代玷污了多重继承的名声。结果许多当下的 OO 语言都没有提供这种功能。
因此,即使你很喜欢复杂的类型树,也完全无法为你的领域准确地建模。
Java 下一切都是类。C++里不使用类还不如使用 C。写 Python、PHP,我们也肯定要时髦地写一些类。写类可以,当你要去继承,你就得考虑清楚了。继承树一旦形成,就是非常 hard to change 的,在敏捷项目里,你要想清楚“代价是什么”,有必要么?这个设计“可逆”么?对于边界清晰的 UI 框架、游戏引擎,使用复杂的继承树,挺好的。对于 UI 逻辑、后台逻辑,可能,你仅仅需要组合、DIP(依赖反转)技术、契约式编程(接口与协议)就够了。写出继承树不是“就应该这么做”,它是成本,继承是要收税的!
在 Golang 下,继承税的烦恼被减轻了,Golang 从来说自己不是 OO 的语言,但是你 OO 的事情,我都能轻松地做到。更进一步,OO 和过程式编程的区别到底是什么?
面向过程,面向对象,函数式编程。三种编程结构的核心区别,是在不同的方向限制程序员,来做到好的代码结构(引自《架构整洁之道》):
▶︎ 结构化编程是对程序控制权的直接转移的限制。
▶︎ 面向对象是对程序控制权的间接转移的限制。
▶︎ 函数式编程是对程序中赋值操作的限制。
SOLID 原则(单一功能、开闭原则、里氏替换、接口隔离、依赖反转,后面会讲到)是 OOP 编程的最经典的原则。其中 D 是指依赖倒置原则(Dependence Inversion Principle),我认为,是 SOLID 里最重要的原则。J2EE 的 container 就是围绕 DIP 原则设计的。DIP 能用于避免构建复杂的继承树,DIP 就是’限制控制权的间接转移’能继续发挥积极作用的最大保障。合理使用 DIP 的 OOP 代码才可能是高质量的代码。
Golang 的 interface 是 duck interface,把 DIP 原则更进一步,不需要显式 implement/extend interface,就能做到 DIP。Golang 使用结构化编程范式,却有面向对象编程范式的核心优点,甚至简化了。这是一个基于高度抽象理解的极度精巧的设计。Google 把 abstraction 这个设计理念发挥到了极致。曾经,J2EE 的 container(EJB, Java Bean)设计是国内 Java 程序员引以为傲“架构设计”、“厉害的设计”。
在 Golang 里,它被分析、解构,以更简单、灵活、统一、易懂的方式呈现出来。写了多年 C++代码的腾讯后端工程师们,是你们再次审视 OOP 的时候了。我大学一年级的时候看的 C++教材,给我描述了一个美好却无法抵达的世界。目标我没有放弃,但我不再用 OOP,而是更多地使用组合(Mixin)。写 Golang 的同学,应该对 DIP 和组合都不陌生,这里我不再赘述。如果有人自傲地说他在 Golang 下搞起了继承,我只能说,“同志,你现在站在了广大 Gopher 的对立面”。现在,你站在哲学的云端,鸟瞰了 Structured Programming 和 OOP。你还愿意再继续支付继承税么?
共享状态是不正确的状态
你坐在最喜欢的餐厅。吃完主菜,问男服务员还有没有苹果派。他回头一看,陈列柜里还有一个,就告诉你“还有”。点到了苹果派,你心满意足地长出了一口气。与此同时,在餐厅的另一边,还有一个顾客也问了女服务员同样的问题。她也看了看,确认有一个,让顾客点了单。总有一个顾客会失望的。
问题出在共享状态。餐厅里的每一个服务员都查看了陈列柜,却没有考虑到其他服务员。你们可以通过加互斥锁来解决正确性的问题,但是,两个顾客有一个会失望或者很久都得不到答案,这是肯定的。
所谓共享状态,换个说法,就是: 由多个人查看和修改状态。这么一说,更好的解决方案就浮出水面了: 将状态改为集中控制。预定苹果派,不再是先查询,再下单。而是有一个餐厅经理负责和服务员沟通,服务员只管发送下单的命令/消息,经理看情况能不能满足服务员的命令。
这种解决方案,换一个说法,也可以说成“用角色实现并发性时不必共享状态”。我们引入了餐厅经理这个角色,赋予了他职责。当然,我们仅仅应该给这个角色发送命令,不应该去询问他。前面讲过了,“只管命令不要询问”,你还记得么。
同时,这个原则就是 golang 里大家耳熟能详的谚语: “不要通过共享内存来通信,而应该通过通信来共享内存”。作为并发性问题的根源,内存的共享备受关注。但实际上,在应用程序代码共享可变资源(文件、数据库、外部服务)的任何地方,问题都有可能冒出来。当代码的两个或多个实例可以同时访问某些资源时,就会出现潜在的问题。
缄默原则
如果一个程序没什么好说,就保持沉默。过多的正常日志,会掩盖错误信息。过多的信息,会让人根本不再关注新出现的信息,“更多信息”变成了“没有信息”。每人添加一点信息,就变成了输出很多信息,最后等于没有任何信息。
▶︎ 不要在正常 case 下打印日志。
▶︎ 不要在单元测试里使用 fmt 标准输出,至少不要提交到 master。
▶︎ 不打不必要的日志。当错误出现的时候,会非常明显,我们能第一时间反应过来并处理。
▶︎ 让调试的日志停留在调试阶段,或者使用较低的日志级别,你的调试信息,对其他人根本没有价值。
▶︎ 即使低级别日志,也不能泛滥。不然,日志打开与否都没有差别,日志变得毫无价值。

错误传递原则
我不喜欢 Java 和 C++的 exception 特性,它容易被滥用,它具有传染性(如果代码 throw 了 excepttion, 你就得 handle 它,不 handle 它,你就崩溃了。可能你不希望崩溃,你仅仅希望报警)。但是 exception(在 golang 下是 panic)是有价值的,参考微软的文章:
Exceptions are preferred in modern C++ for the following reasons:
* An exception forces calling code to recognize an error condition and handle it. Unhandled exceptions stop program execution.
* An exception jumps to the point in the call stack that can handle the error. Intermediate functions can let the exception propagate. They don't have to coordinate with other layers.
* The exception stack-unwinding mechanism destroys all objects in scope after an exception is thrown, according to well-defined rules.
* An exception enables a clean separation between the code that detects the error and the code that handles the error.
Google 的 C++规范在常规情况禁用 exception,理由包含如下内容:
Because most existing C++ code at Google is not prepared to deal with exceptions, it is comparatively difficult to adopt new code that generates exceptions.
从 google 和微软的文章中,我们不难总结出以下几点衍生的结论:
▶︎ 在必要的时候抛出 exception。使用者必须具备“必要性”的判断能力。
▶︎ exception 能一路把底层的异常往上传递到高函数层级,信息被向上传递,并且在上级被妥善处理。可以让异常和关心具体异常的处理函数在高层级和低层级遥相呼应,中间层级什么都不需要做,仅仅向上传递。
▶︎ exception 传染性很强。当代码由多人协作,使用 A 模块的代码都必须要了解它可能抛出的异常,做出合理的处理。不然,就都写一个丑陋的 catch,catch 所有异常,然后做一个没有针对性的处理。每次 catch 都需要加深一个代码层级,代码常常写得很丑。
我们看到了异常的优缺点。上面第二点提到的信息传递,是很有价值的一点。golang 在 1.13 版本中拓展了标准库,支持了Error Wrapping也是承认了 error 传递的价值。
所以,我们认为错误处理,应该具备跨层级的错误信息传递能力,中间层级如果不关心,就把 error 加上本层的信息向上透传(有时候可以直接透传),应该使用 Error Wrapping。exception/panic 具有传染性,大量使用,会让代码变得丑陋,同时容易滋生可读性问题。我们应该多使用 Error Wrapping,在必要的时候,才使用 exception/panic。每一次使用 exception/panic,都应该被认真审核。需要 panic 的地方,不去 panic,也是有问题的。参考本文的“尽早崩溃”。
额外说一点,注意不要把整个链路的错误信息带到公司外,带到用户的浏览器、native 客户端。至少不能直接展示给用户看到。

SOLID
SOLID 原则,是以下几个原则的集合体:
▶︎ SRP:单一职责原则;
▶︎ OCP:开闭原则;
▶︎ LSP:里氏替换原则;
▶︎ ISP:接口隔离原则;
▶︎ DIP:依赖反转原则。
这些年来,这几个设计原则在很多不同的出版物里都有过详细描述。它们太出名了,我这里就不做详解了。我想说的是,这 5 个原则环环相扣,前 4 个原则,要么就是同时做到,要么就是都没做到,很少有说,做到其中一点其他三点都不满足。ISP 就是做到 LSP 的常用手段。ISP 也是做到 DIP 的基础。只是,它刚被提出来的时候,是主要针对“设计继承树”这个目的的。现在,它们已经被更广泛地使用在模块、领域、组件这种更大的概念上。
SOLI 都显而易见,DIP 原则是最值得注意的一点,我在其他原则里也多次提到了它。如果你还不清楚什么是 DIP,一定去看明白。这是工程师最基础、必备的知识点之一了。
要做到 OCP 开闭原则,其实,就是要大家要通过后面讲到的“不要面向需求编程”才能做好。如果你还是面向需求、面向 UI、交互编程,你永远做不到开闭,并且不知道如何才能做到开闭。
如果你对这些原则确实不了解,建议读一读《架构整洁之道》。该书的作者 Bob 大叔,就是第一个提出 SOLID 这个集合体的人(20 世纪 80 年代末,在 USENET 新闻组)。
一个函数不要出现多个层级的代码
// IrisFriends 拉取好友
func IrisFriends(ctx iris.Context, app *app.App) {
var rsp sdc.FriendsRsp
defer func() {
var buf
bytes.Buffer _ = (&jsonpb.Marshaler{EmitDefaults: true})
.Marshal(&buf, &rsp)
_, _ = ctx.Write(buf.Bytes())
}()
common.AdjustCookie(ctx)
if !checkCookie(ctx) {
return
}
// 从cookie中拿到关键的登陆态等有效信息
var session common.BaseSession common.GetBaseSessionFromCookie(ctx, &session)
// 校验登陆态
err := common.CheckLoginSig(session, app.ConfigStore.Get().OIDBCmdSetting.PTLogin) if err != nil {
_ = common.ErrorResponse(ctx, errors.PTSigErr, 0, "check login sig error") return
}
if err = getRelationship(ctx, app.ConfigStore.Get().OIDBCmdSetting, NewAPI(), &rsp); err != nil {
// TODO:日志
}
return
}
上面这一段代码,是我随意找的一段代码。逻辑非常清晰,因为除了最上面 defer 写回包的代码,其他部分都是顶层函数组合出来的。阅读代码,我们不会掉到细节里出不来,反而忽略了整个业务流程。同时,我们能明显发现它没写完,以及 common.ErrorResponse 和 defer func 两个地方都写了回包,可能出现发起两次 http 回包。TODO 也会非常显眼。
想象一下,我们没有把细节收归进 checkCookie()、getRelationship()等函数,而是展开在这里,但是总函数行数没有到 80 行,表面上符合规范。但是实际上,阅读代码的同学不再能轻松掌握业务逻辑,而是同时在阅读功能细节和业务流程。阅读代码变成了每个时刻心智负担都很重的事情。
显而易见,单个函数里应该只保留某一个层级(layer)的代码,更细化的细节应该被抽象到下一个 layer 去,成为子函数。
Unix 哲学基础
这一句话改成:Unix 的设计哲学,值得大家深入阅读学习。最后我也想挑几条原则展开跟大家分享一下这些经典的智慧。
▶︎ 模块原则:使用简洁的接口拼合简单的部件;
▶︎ 清晰原则:清晰胜于技巧;
▶︎ 组合原则:设计时考虑拼接组合;
▶︎ 分离原则:策略同机制分离,接口同引擎分离;
▶︎ 简洁原则:设计要简洁,复杂度能低则低;
▶︎ 吝啬原则:除非确无它法,不要编写庞大的程序;
▶︎ 透明性原则:设计要可见,以便审查和调试;
▶︎ 健壮原则:健壮源于透明与简洁;
▶︎ 表示原则:把知识叠入数据以求逻辑质朴而健壮;
▶︎ 通俗原则:接口设计避免标新立异;
▶︎ 缄默原则:如果一个程序没什么好说,就保持沉默;
▶︎ 补救原则:出现异常时,马上退出并给出足量错误信息;
▶︎ 经济原则:宁花机器一分,不花程序员一秒;
▶︎ 生成原则:避免手工 hack,尽量编写程序去生成程序;
▶︎ 优化原则:雕琢前先得有原型,跑之前先学会走;
▶︎ 多样原则:绝不相信所谓"不二法门"的断言;
▶︎ 扩展原则:设计着眼未来,未来总比预想快。
KISS 原则,大家应该是如雷贯耳了。但是,你真的在遵守?什么是 Simple?简单?Golang 语言主要设计者之一的 Rob Pike 说“大道至简”,这个“简”和简单是一个意思么?
首先,简单不是面对一个问题,我们印入眼帘第一映像的解法为简单。我说一句,感受一下。“把一个事情做出来容易,把事情用最简单有效的方法做出来,是一个很难的事情。”比如,做一个三方授权,oauth2.0 很简单,所有概念和细节都是紧凑、完备、易用的。你觉得要设计到 oauth2.0 这个效果很容易么?要做到简单,就要对自己处理的问题有全面的了解,然后需要不断积累思考,才能做到从各个角度和层级去认识这个问题,打磨出一个通俗、紧凑、完备的设计,就像 ios 的交互设计。简单不是容易做到的,需要大家在不断的时间和 Code Review 过程中去积累思考,pk 中触发思考,交流中总结思考,才能做得愈发地好,接近“大道至简”。
两张经典的模型图,简单又全面,感受一下,没看懂,可以立即自行 Google 学习一下:RBAC:

logging:

使用组合,其实就是要让你明确清楚自己现在所拥有的是哪个部件。如果部件过于多,其实完成组合最终成品这个步骤,就会有较高的心智负担,每个部件展开来,琳琅满目,眼花缭乱。比如 QT 这个通用 UI 框架,看它的 Class 列表,有 1000 多个。如果不用继承树把它组织起来,平铺展开,组合出一个页面,将会变得心智负担高到无法承受。OOP 在“需要无数元素同时展现出来”这种复杂度极高的场景,有效的控制了复杂度 。“那么古尔丹,代价是什么呢?”代价就是,一开始做出这个自上而下的设计,牵一发而动全身,每次调整都变得异常困难。
实际项目中,各种职业级别不同的同学一起协作修改一个 server 的代码,就会出现,职级低的同学改哪里都改不对,根本没能力进行修改,高级别的同学能修改对,也不愿意大规模修改,整个项目变得愈发不合理。对整个继承树没有完全认识的同学都没有资格进行任何一个对继承树有调整的修改,协作变得寸步难行。代码的修改,都变成了依赖一个高级架构师高强度监控继承体系的变化,低级别同学们束手束脚的结果。组合,就很好的解决了这个问题,把问题不断细分,每个同学都可以很好地攻克自己需要攻克的点,实现一个 package。产品逻辑代码,只需要去组合各个 package,就能达到效果。
这是 golang 标准库里 http request 的定义,它就是 Http 请求所有特性集合出来的结果。其中通用/异变/多种实现的部分,通过 duck interface 抽象,比如 Body io.ReadCloser。你想知道哪些细节,就从组合成 request 的部件入手,要修改,只需要修改对应部件。[这段代码后,对比.NET 的 HTTP 基于 OOP 的抽象]
// A Request represents an HTTP request received by a server
// or to be sent by a client.
//
// The field semantics differ slightly between client and server
// usage. In addition to the notes on the fields below, see the
// documentation for Request.Write and RoundTripper.
type Request struct {
// Method specifies the HTTP method (GET, POST, PUT, etc.).
// For client requests, an empty string means GET.
//
// Go's HTTP client does not support sending a request with
// the CONNECT method. See the documentation on Transport for
// details.
Method string
// URL specifies either the URI being requested (for server
// requests) or the URL to access (for client requests).
//
// For server requests, the URL is parsed from the URI
// supplied on the Request-Line as stored in RequestURI. For
// most requests, fields other than Path and RawQuery will be
// empty. (See RFC 7230, Section 5.3)
//
// For client requests, the URL's Host specifies the server to
// connect to, while the Request's Host field optionally
// specifies the Host header value to send in the HTTP
// request.
URL *url.URL
// The protocol version for incoming server requests.
//
// For client requests, these fields are ignored. The HTTP
// client code always uses either HTTP/1.1 or HTTP/2.
// See the docs on Transport for details.
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
// Header contains the request header fields either received
// by the server or to be sent by the client.
//
// If a server received a request with header lines,
//
// Host: example.com
// accept-encoding: gzip, deflate
// Accept-Language: en-us
// fOO: Bar
// foo: two
//
// then
//
// Header = map[string][]string{
// "Accept-Encoding": {"gzip, deflate"},
// "Accept-Language": {"en-us"},
// "Foo": {"Bar", "two"},
// }
//
// For incoming requests, the Host header is promoted to the
// Request.Host field and removed from the Header map.
//
// HTTP defines that header names are case-insensitive. The
// request parser implements this by using CanonicalHeaderKey,
// making the first character and any characters following a
// hyphen uppercase and the rest lowercase.
//
// For client requests, certain headers such as Content-Length
// and Connection are automatically written when needed and
// values in Header may be ignored. See the documentation
// for the Request.Write method.
Header Header
// Body is the request's body.
//
// For client requests, a nil body means the request has no
// body, such as a GET request. The HTTP Client's Transport
// is responsible for calling the Close method.
//
// For server requests, the Request Body is always non-nil
// but will return EOF immediately when no body is present.
// The Server will close the request body. The ServeHTTP
// Handler does not need to.
Body io.ReadCloser
// GetBody defines an optional func to return a new copy of
// Body. It is used for client requests when a redirect requires
// reading the body more than once. Use of GetBody still
// requires setting Body.
//
// For server requests, it is unused.
GetBody func() (io.ReadCloser, error)
// ContentLength records the length of the associated content.
// The value -1 indicates that the length is unknown.
// Values >= 0 indicate that the given number of bytes may
// be read from Body.
//
// For client requests, a value of 0 with a non-nil Body is
// also treated as unknown.
ContentLength int64
// TransferEncoding lists the transfer encodings from outermost to
// innermost. An empty list denotes the "identity" encoding.
// TransferEncoding can usually be ignored; chunked encoding is
// automatically added and removed as necessary when sending and
// receiving requests.
TransferEncoding []string
// Close indicates whether to close the connection after
// replying to this request (for servers) or after sending this
// request and reading its response (for clients).
//
// For server requests, the HTTP server handles this automatically
// and this field is not needed by Handlers.
//
// For client requests, setting this field prevents re-use of
// TCP connections between requests to the same hosts, as if
// Transport.DisableKeepAlives were set.
Close bool
// For server requests, Host specifies the host on which the
// URL is sought. For HTTP/1 (per RFC 7230, section 5.4), this
// is either the value of the "Host" header or the host name
// given in the URL itself. For HTTP/2, it is the value of the
// ":authority" pseudo-header field.
// It may be of the form "host:port". For international domain
// names, Host may be in Punycode or Unicode form. Use
// golang.org/x/net/idna to convert it to either format if
// needed.
// To prevent DNS rebinding attacks, server Handlers should
// validate that the Host header has a value for which the
// Handler considers itself authoritative. The included
// ServeMux supports patterns registered to particular host
// names and thus protects its registered Handlers.
//
// For client requests, Host optionally overrides the Host
// header to send. If empty, the Request.Write method uses
// the value of URL.Host. Host may contain an international
// domain name.
Host string
// Form contains the parsed form data, including both the URL
// field's query parameters and the PATCH, POST, or PUT form data.
// This field is only available after ParseForm is called.
// The HTTP client ignores Form and uses Body instead.
Form url.Values
// PostForm contains the parsed form data from PATCH, POST
// or PUT body parameters.
//
// This field is only available after ParseForm is called.
// The HTTP client ignores PostForm and uses Body instead.
PostForm url.Values
// MultipartForm is the parsed multipart form, including file uploads.
// This field is only available after ParseMultipartForm is called.
// The HTTP client ignores MultipartForm and uses Body instead.
MultipartForm *multipart.Form
// Trailer specifies additional headers that are sent after the request
// body.
//
// For server requests, the Trailer map initially contains only the
// trailer keys, with nil values. (The client declares which trailers it
// will later send.) While the handler is reading from Body, it must
// not reference Trailer. After reading from Body returns EOF, Trailer
// can be read again and will contain non-nil values, if they were sent
// by the client.
//
// For client requests, Trailer must be initialized to a map containing
// the trailer keys to later send. The values may be nil or their final
// values. The ContentLength must be 0 or -1, to send a chunked request.
// After the HTTP request is sent the map values can be updated while
// the request body is read. Once the body returns EOF, the caller must
// not mutate Trailer.
//
// Few HTTP clients, servers, or proxies support HTTP trailers.
Trailer Header
// RemoteAddr allows HTTP servers and other software to record
// the network address that sent the request, usually for
// logging. This field is not filled in by ReadRequest and
// has no defined format. The HTTP server in this package
// sets RemoteAddr to an "IP:port" address before invoking a
// handler.
// This field is ignored by the HTTP client.
RemoteAddr string
// RequestURI is the unmodified request-target of the
// Request-Line (RFC 7230, Section 3.1.1) as sent by the client
// to a server. Usually the URL field should be used instead.
// It is an error to set this field in an HTTP client request.
RequestURI string
// TLS allows HTTP servers and other software to record
// information about the TLS connection on which the request
// was received. This field is not filled in by ReadRequest.
// The HTTP server in this package sets the field for
// TLS-enabled connections before invoking a handler;
// otherwise it leaves the field nil.
// This field is ignored by the HTTP client.
TLS *tls.ConnectionState
// Cancel is an optional channel whose closure indicates that the client
// request should be regarded as canceled. Not all implementations of
// RoundTripper may support Cancel.
//
// For server requests, this field is not applicable.
//
// Deprecated: Set the Request's context with NewRequestWithContext
// instead. If a Request's Cancel field and context are both
// set, it is undefined whether Cancel is respected.
Cancel <-chan struct{}
// Response is the redirect response which caused this request
// to be created. This field is only populated during client
// redirects.
Response *Response
// ctx is either the client or server context. It should only
// be modified via copying the whole Request using WithContext.
// It is unexported to prevent people from using Context wrong
// and mutating the contexts held by callers of the same request.
ctx context.Context
}
看看.NET 里对于 web 服务的抽象,仅仅看到末端,不去看完整个继承树的完整图景,我根本无法知道我关心的某个细节在什么位置。进而,我要往整个 http 服务体系里修改任何功能,都无法抛开对整体完整设计的理解和熟悉,还极容易没有知觉地破坏者整体的设计。
说到组合,还有一个关系很紧密的词,叫插件化。大家都用 VS code 用得很开心,它比 Visual Studio 成功在哪里?如果 VS code 通过添加一堆插件达到 Visual Studio 具备的能力,那么它将变成另一个和 Visual Studio 差不多的东西,叫做 VS Studio 吧。大家应该发现问题了,我们很多时候其实并不需要 Visual Studio 的大多数功能,而且希望灵活定制化一些比较小众的能力,用一些小众的插件。甚至,我们希望选择不同实现的同类型插件。这就是组合的力量,各种不同的组合,它简单,却又满足了各种需求,灵活多变,要实现一个插件,不需要事先掌握一个庞大的体系。体现在代码上,也是一样的道理。至少后端开发领域,组合,比 OOP,“香”很多。
可能有些同学会觉得,把程序写得庞大一些才好拿得出手去评高级职称。leader 们一看评审方案就容易觉得:很大,很好,很全面。但是,我们真的需要写这么大的程序么?
我又要说了“那么古尔丹,代价是什么呢?”。代价是代码越多,越难维护,难调整。C 语言之父 Ken Thompson 说“删除一行代码,给我带来的成就感要比添加一行要大”。我们对于代码,要吝啬。能把系统做小,就不要做大。腾讯不乏 200w+行的客户端,很大,很牛。但是,同学们自问,现在还调整得动架构么。能小做的事情就小做,寻求通用化,通过 duck interface(甚至多进程,用于隔离能力的多线程)把模块、能力隔离开,时刻想着删减代码量,才能保持代码的可维护性和面对未来的需求、架构,调整自身的活力。客户端代码,UI 渲染模块可以复杂吊炸天,非 UI 部分应该追求最简单,能力接口化,可替换、重组合能力强。
落地到大家的代码,review 时,就应该最关注核心 struct 定义,构建起一个完备的模型,核心 interface,明确抽象 model 对外部的依赖,明确抽象 model 对外提供的能力。其他代码,就是要用最简单、平平无奇的代码实现模型内部细节。
首先,定义一下,什么是透明性和可显性。
“如果没有阴暗的角落和隐藏的深度,软件系统就是透明的。透明性是一种被动的品质。如果实际上能预测到程序行为的全部或大部分情况,并能建立简单的心理模型,这个程序就是透明的,因为可以看透机器究竟在干什么。
如果软件系统所包含的功能是为了帮助人们对软件建立正确的“做什么、怎么做”的心理模型而设计,这个软件系统就是可显的。因此,举例来说,对用户而言,良好的文档有助于提高可显性;对程序员而言,良好的变量和函数名有助于提高可显性。可显性是一种主动品质。在软件中要达到这一点,仅仅做到不晦涩是不够的,还必须要尽力做到有帮助。”
我们要写好程序,减少 bug,就要增强自己对代码的控制力。你始终做到,理解自己调用的函数/复用的代码大概是怎么实现的。不然,你可能就会在单线程状态机的 server 里调用有 IO 阻塞的函数,让自己的 server 吞吐量直接掉到底。进而,为了保证大家能对自己代码能做到有控制力,所有人写的函数,就必须具备很高的透明性。而不是写一些看了一阵看不明白的函数/代码,结果被迫使用你代码的人,直接放弃了对掌控力的追取,甚至放弃复用你的代码,另起炉灶,走向了’制造重复代码’的深渊。
透明性其实相对容易做到的,大家有意识地锻炼一两个月,就能做得很好。可显性就不容易了。有一个现象是,你写的每一个函数都不超过 80 行,每一行我都能看懂,但是你层层调用,很多函数调用,组合起来怎么就实现了某个功能,看两遍,还是看不懂。第三遍可能才能大概看懂。大概看懂了,但太复杂,很难在大脑里构建起你实现这个功能的整体流程。结果就是,阅读者根本做不到对你的代码有好的掌控力。
可显性的标准很简单,大家看一段代码,懂不懂,一下就明白了。但是,如何做好可显性?那就是要追求合理的函数分组,合理的函数上下级层次,同一层次的代码才会出现在同一个函数里,追求通俗易懂的函数分组分层方式,是通往可显性的道路。
当然,复杂如 linux 操作系统,office 文档,问题本身就很复杂,拆解、分层、组合得再合理,都难建立心理模型。这个时候,就需要完备的文档了。完备的文档还需要出现在离代码最近的地方,让人“知道这里复杂的逻辑有文档”,而不是其实文档,但是阅读者不知道。再看看上面 Golang 标准库里的 http.Request,感受到它在可显性上的努力了么?对,就去学它。
设计程序过于标新立异的话,可能会提升别人理解的难度。
一般,我们这么定义一个“点”,使用 x 表示横坐标,用 y 表示纵坐标:
type Point struct {
X float64
Y float64
}
你就是要不同、精准:
type Point struct {
VerticalOrdinate float64
HorizontalOrdinate float64
}
很好,你用词很精准,一般人还驳斥不了你。但是,多数人读你的 VerticalOrdinate 就是没有读 X 理解来得快,来得容易懂、方便。你是在刻意制造协作成本。
上面的例子常见,但还不是最小立异原则最想说明的问题。想想一下,一个程序里,你把用“+”这个符号表示数组添加元素,而不是数学“加”,“result := 1+2” –> “result = []int{1, 2}”而不是“result=3”,那么,你这个标新立异,对程序的破坏性,简直无法想象。“最小立异原则的另一面是避免表象想死而实际却略有不同。这会极端危险,因为表象相似往往导致人们产生错误的假定。所以最好让不同事物有明显区别,而不要看起来几乎一模一样。” – Henry Spencer。
你实现一个 db.Add()函数却做着 db.AddOrUpdate()的操作,有人使用了你的接口,错误地把数据覆盖了。
这个原则,应该是大家最经常破坏的原则之一。一段简短的代码里插入了各种“log(“cmd xxx enter”)”, “log(“req data " + req.String())”,非常害怕自己信息打印得不够。害怕自己不知道程序执行成功了,总要最后“log(“success”)”。但是,我问一下大家,你们真的耐心看过别人写的代码打的一堆日志么?不是自己需要哪个,就在一堆日志里,再打印一个日志出来一个带有特殊标记的日志“log(“this_is_my_log_” + xxxxx)”?结果,第一个作者打印的日志,在代码交接给其他人或者在跟别人协作的时候,这个日志根本没有价值,反而提升了大家看日志的难度。
一个服务一跑起来,就疯狂打日志,请求处理正常也打一堆日志。滚滚而来的日志,把错误日志淹没在里面。错误日志失去了效果,简单地 tail 查看日志,眼花缭乱,看不出任何问题,这不就成了“为了捕获问题”而让自己“根本无法捕获问题”了么?
沉默是金。除了简单的 stat log,如果你的程序’发声’了,那么它抛出的信息就一定要有效!打印一个 log(‘process fail’)也是毫无价值,到底什么 fail 了?是哪个用户带着什么参数在哪个环节怎么 fail 了?如果发声,就要把必要信息给全。不然就是不发声,表示自己好好地 work 着呢。不发声就是最好的消息,现在我的 work 一切正常!
“设计良好的程序将用户的注意力视为有限的宝贵资源,只有在必要时才要求使用。”程序员自己的主力,也是宝贵的资源!只有有必要的时候,日志才跑来提醒程序员“我有问题,来看看”,而且,必须要给到足够的信息,让一把讲明白现在发生了什么。而不是程序员还需要很多辅助手段来搞明白到底发生了什么。
每当我发布程序 ,我抽查一个机器,看它的日志。发现只有每分钟外部接入、内部 rpc 的个数/延时分布日志的时候,我就心情很愉悦。我知道,这一分钟,它的成功率又是 100%,没任何问题!
其实这个问题很简单,如果出现异常,异常并不会因为我们尝试掩盖它,它就不存在了。所以,程序错误和逻辑错误要严格区分对待。这是一个态度问题。
“异常是互联网服务器的常态”。逻辑错误通过 metrics 统计,我们做好告警分析。对于程序错误 ,我们就必须要严格做到在问题最早出现的位置就把必要的信息搜集起来,高调地告知开发和维护者“我出现异常了,请立即修复我!”。可以是直接就没有被捕获的 panic 了。也可以在一个最上层的位置统一做好 recover 机制,但是在 recover 的时候一定要能获得准确异常位置的准确异常信息。不能有中间 catch 机制,catch 之后丢失很多信息再往上传递。
很多 Java 开发的同学,不区分程序错误和逻辑错误,要么都很宽容,要么都很严格,对代码的可维护性是毁灭性的破坏。“我的程序没有程序错误,如果有,我当时就解决了。”只有这样,才能保持程序代码质量的相对稳定,在火苗出现时扑灭火灾是最好的扑灭火灾的方式。当然,更有效的方式是全面自动化测试的预防:)
本文主要阐述了研发人员在日常工作和职业生涯中,或多或少都会去学习并运用的知名架构原则,并从我个人的角度去做了深入的发散阐述。在下一篇文章中,我将从程序员的自我修养和不能上升到原则的几个常见案例来继续阐述程序员修炼之道的未尽事宜。
作为程序开发者,避免不了阅读别人代码,那么就会涉及到到一门语言的编程规范。规范虽然不是语言本身的硬性要求,但是已经是每一个语言使用者约定俗成的一个规范。
按照编程规范编写的代码,至少在代码阅读时,给人一种愉悦的心情,特别是强迫症患者。另一方面,统一的编程风格,可以减少编写错误,利于后期维护。
因为最近又开始进行纯C语言的开发,并且是基于SDK的开发,所以添加的每一行代码都应该与原来风格保持一致,不能因为一颗老鼠屎坏了一锅汤。一个良好的编程规范也可以看出编程人员的细心程度与代码质量。
之前待过的两家公司,也都有各自总结的编程规范,但都不约而同的一致,适用本公司的软件开发。这几天有幸可以参阅华为技术有限公司的C语言编程规范,相比之下,写的更加详细。
至少接触到了,在这个编程规范中体现了,并且还扩充了很多,我觉得有必要归纳总结,一遍日后查阅。先是学习规范,然后再积累规范,最后才是依规范编写。
清晰性是易于维护、易于重构的程序必需具备的特征。代码首先是给人读的,好的代码应当可以像文章一样发声朗诵出来。
简洁就是易于理解并且易于实现。代码越长越难以看懂,也就越容易在修改时引入错误。写的代码越多,意味着出错的地方越多,也就意味着代码的可靠性越低。因此,我们提倡大家通过编写简洁明了的代码来提升代码可靠性。废弃的代码(没有被调用的函数和全局变量)要及时清除,重复代码应该尽可能提炼成函数。
产品所有人共同分享同一种风格所带来的好处,远远超出为了统一而付出的代价。在公司已有编码规范的指导下,审慎地编排代码以使代码尽可能清晰,是一项非常重要的技能。如果重构/修改其他风格的代码时,比较明智的做法是根据现有代码的现有风格继续编写代码,或者使用格式转换工具进行转换成公司内部风格。
原则1.1 头文件中适合放置接口的声明,不适合放置实现。
说明:头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。
原则1.2 头文件应当职责单一。
说明:头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。很多现有代码中头文件过大,职责过多,再加上循环依赖的问题,可能导致为了在.c中使用一个宏,而包含十几个头文件。
原则1.3 头文件应向稳定的方向包含。
说明:头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。
规则1.1 每一个.c文件应有一个同名.h文件,用于声明需要对外公开的接口。
说明:如果一个.c文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如main函数所在的文件。
规则1.2 禁止头文件循环依赖。
说明:头文件循环依赖,指a.h包含b.h,b.h包含c.h,c.h包含a.h之类导致任何一个头文件修改,都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。
而如果是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h/c.h的源代码重新编译。
规则1.3 .c/.h文件禁止包含用不到的头文件。
说明:很多系统中头文件包含关系复杂,开发人员为了省事起见,可能不会去一一钻研,直接包含一切想到的头文件,甚至有些产品干脆发布了一个god.h,其中包含了所有头文件,然后发布给各个项目组使用,这种只图一时省事的做法,导致整个系统的编译时间进一步恶化,并对后来人的维护造成了巨大的麻烦。
规则1.4 头文件应当自包含。
说明:简单的说,自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户增添不必要的负担。
规则1.5 总是编写内部#include保护符(#define 保护)。
说明:多次包含一个头文件可以通过认真的设计来避免。如果不能做到这一点,就需要采取阻止头文件内容被包含多于一次的机制。
注: 没有在宏最前面加上 _ ,即使用 FILENAME_H代替 FILENAME_H ,是因为一般以 _ 和 __ 开头的标识符为系统保留或者标准库使用,在有些静态检查工具中,若全局可见的标识符以 _ 开头会给出告警。
定义包含保护符时,应该遵守如下规则:
1)保护符使用唯一名称;
2)不要在受保护部分的前后放置代码或者注释。
规则1.6 禁止在头文件中定义变量。
说明:在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义。
规则1.7 只能通过包含头文件的方式使用其他.c提供的接口,禁止在.c中通过extern的方式使用外部函数接口、变量。
说明:若a.c使用了b.c定义的foo()函数,则应当在b.h中声明extern int foo(int input);并在a.c中通过#include <b.h>来使用foo。禁止通过在a.c中直接写extern int foo(int input);来使用foo,后面这种写法容易在foo改变时可能导致声明和定义不一致。这一点我们因为图方便经常犯的。
规则1.8 禁止在extern “C"中包含头文件。
说明:在extern “C"中包含头文件,会导致extern “C"嵌套,Visual Studio对extern “C"嵌套层次有限制,嵌套层次太多会编译错误。
建议1.1 一个模块通常包含多个.c文件,建议放在同一个目录下,目录名即为模块名。为方便外部使用者,建议每一个模块提供一个.h,文件名为目录名。
建议1.2 如果一个模块包含多个子模块,则建议每一个子模块提供一个对外的.h,文件名为子模块名。
建议1.3 头文件不要使用非习惯用法的扩展名,如.inc。
建议1.4 同一产品统一包含头文件排列方式。
原则2.1 一个函数仅完成一件功能。
说明:一个函数实现多个功能给开发、使用、维护都带来很大的困难。
原则2.2 重复代码应该尽可能提炼成函数。
说明:重复代码提炼成函数可以带来维护成本的降低。
规则2.1 避免函数过长,新增函数不超过50行(非空非注释行)。
说明:本规则仅对新增函数做要求,对已有函数修改时,建议不增加代码行。
规则2.2 避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层。
说明:本规则仅对新增函数做要求,对已有的代码建议不增加嵌套层次。
规则2.3 可重入函数应避免使用共享变量;若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护。
规则2.4 对参数的合法性检查,由调用者负责还是由接口函数负责,应在项目组/模块内应统一规定。缺省由调用者负责。
规则2.5 对函数的错误返回码要全面处理。
规则2.6 设计高扇入,合理扇出(小于7)的函数。
说明:扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。如下图:

规则2.7 废弃代码(没有被调用的函数和变量)要及时清除。
建议2.1 函数不变参数使用const。
说明:不变的值更易于理解/跟踪和分析,把const作为默认选项,在编译时会对其进行检查,使代码更牢固/更安全。
建议2.2 函数应避免使用全局变量、静态局部变量和I/O操作,不可避免的地方应集中使用。
建议2.3 检查函数所有非参数输入的有效性,如数据文件、公共变量等。
说明:函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入参数之前,应进行有效性检查。
建议2.4 函数的参数个数不超过5个。
建议2.5 除打印类函数外,不要使用可变长参函数。
建议2.6 在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static关键字。
目前比较常用的如下几种命名风格:
unix like风格:单词用小写字母,每个单词直接用下划线_分割,,例如text_mutex,kernel_text_address。
Windows风格:大小写字母混用,单词连在一起,每个单词首字母大写。不过Windows风格如果遇到大写专有用语时会有些别扭,例如命名一个读取RFC文本的函数,命令为ReadRFCText,看起来就没有unix like的read_rfc_text清晰了。
原则3.1 标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解。
原则3.2 除了常见的通用缩写以外,不使用单词缩写,不得使用汉语拼音。
建议3.1 产品/项目组内部应保持统一的命名风格。
建议3.2 尽量避免名字中出现数字编号,除非逻辑上的确需要编号。
建议3.3 标识符前不应添加模块、项目、产品、部门的名称作为前缀。
建议3.4 平台/驱动等适配代码的标识符命名风格保持和平台/驱动一致。
建议3.5 重构/修改部分代码时,应保持和原有代码的命名风格一致。
建议3.6 文件命名统一采用小写字符。
规则3.2 全局变量应增加“g_”前缀。
规则3.3 静态变量应增加“s_”前缀。
规则3.4 禁止使用单字节命名变量,但允许定义i、j、k作为局部循环变量。
建议3.7 不建议使用匈牙利命名法。
说明:变量命名需要说明的是变量的含义,而不是变量的类型。在变量命名前增加类型说明,反而降低了变量的可读性;更麻烦的问题是,如果修改了变量的类型定义,那么所有使用该变量的地方都需要修改。
建议3.8 使用名词或者形容词+名词方式命名变量。
建议3.9 函数命名应以函数要执行的动作命名,一般采用动词或者动词+名词的结构。
建议3.10 函数指针除了前缀,其他按照函数的命名规则命名。
规则3.5 对于数值或者字符串等等常量的定义,建议采用全大写字母,单词之间加下划线„_‟的方式命名(枚举同样建议使用此方式定义)。
规则3.6 除了头文件或编译开关等特殊标识定义,宏定义不能使用下划线„_‟开头和结尾。
原则4.1 一个变量只有一个功能,不能把一个变量用作多种用途。
原则4.2 结构功能单一;不要设计面面俱到的数据结构。
原则4.3 不用或者少用全局变量。
规则4.1 防止局部变量与全局变量同名。
规则4.2 通讯过程中使用的结构,必须注意字节序。
规则4.3 严禁使用未经初始化的变量作为右值。
建议4.1 构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象。
建议4.2 使用面向接口编程思想,通过API访问数据:如果本模块的数据需要对外部模块开放,应提供接口函数来设置、获取,同时注意全局数据的访问互斥。
建议4.3 在首次使用前初始化变量,初始化的地方离使用的地方越近越好。
建议4.4 明确全局变量的初始化顺序,避免跨模块的初始化依赖。
说明:系统启动阶段,使用全局变量前,要考虑到该全局变量在什么时候初始化,使用全局变量和初始化全局变量,两者之间的时序关系,谁先谁后,一定要分析清楚,不然后果往往是低级而又灾难性的。
建议4.5 尽量减少没有必要的数据类型默认转换与强制转换。
说明:当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患。
规则5.1 用宏定义表达式时,要使用完备的括号。
说明:因为宏只是简单的代码替换,不会像函数一样先将参数计算后,再传递。
规则5.2 将宏所定义的多条表达式放在大括号中。
说明:更好的方法是多条语句写成do while(0)的方式。
规则5.3 使用宏时,不允许参数发生变化。
规则5.4 不允许直接使用魔鬼数字。
说明:使用魔鬼数字的弊端:代码难以理解;如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重。使用明确的物理状态或物理意义的名称能增加信息,并能提供单一的维护点。
建议5.1 除非必要,应尽可能使用函数代替宏。
说明:宏对比函数,有一些明显的缺点:宏缺乏类型检查,不如函数调用检查严格。
建议5.2 常量建议使用const定义代替宏。
建议5.3 宏定义中尽量不使用return、goto、continue、break等改变程序流程的语句。
原则6.1 代码质量保证优先原则
(1)正确性,指程序要实现设计要求的功能。
(2)简洁性,指程序易于理解并且易于实现。
(3)可维护性,指程序被修改的能力,包括纠错、改进、新需求或功能规格变化的适应能力。
(4)可靠性,指程序在给定时间间隔和环境条件下,按设计要求成功运行程序的概率。
(5)代码可测试性,指软件发现故障并隔离、定位故障的能力,以及在一定的时间和成本前提下,进行测试设计、测试执行的能力。
(6)代码性能高效,指是尽可能少地占用系统资源,包括内存和执行时间。
(7)可移植性,指为了在原来设计的特定环境之外运行,对系统进行修改的能力。
(8)个人表达方式/个人方便性,指个人编程习惯。
原则6.2 要时刻注意易混淆的操作符。比如说一些符号特性、计算优先级。
原则6.3 必须了解编译系统的内存分配方式,特别是编译系统对不同类型的变量的内存分配规则,如局部变量在何处分配、静态变量在何处分配等。
原则6.4 不仅关注接口,同样要关注实现。
说明:这个原则看似和“面向接口”编程思想相悖,但是实现往往会影响接口,函数所能实现的功能,除了和调用者传递的参数相关,往往还受制于其他隐含约束,如:物理内存的限制,网络状况,具体看“抽象漏洞原则”。
规则6.1 禁止内存操作越界。
坚持下列措施可以避免内存越界:
数组的大小要考虑最大情况,避免数组分配空间不够。
避免使用危险函数sprintf /vsprintf/strcpy/strcat/gets操作字符串,使用相对安全的函数snprintf/strncpy/strncat/fgets代替。
使用memcpy/memset时一定要确保长度不要越界
字符串考虑最后的’\0’, 确保所有字符串是以’\0’结束
指针加减操作时,考虑指针类型长度
数组下标进行检查
使用时sizeof或者strlen计算结构/字符串长度,,避免手工计算
坚持下列措施可以避免内存泄漏:
异常出口处检查内存、定时器/文件句柄/Socket/队列/信号量/GUI等资源是否全部释放
删除结构指针时,必须从底层向上层顺序删除
使用指针数组时,确保在释放数组时,数组中的每个元素指针是否已经提前被释放了
避免重复分配内存
小心使用有return、break语句的宏,确保前面资源已经释放
检查队列中每个成员是否释放
规则6.3 禁止引用已经释放的内存空间。
坚持下列措施可以避免引用已经释放的内存空间:
内存释放后,把指针置为NULL;使用内存指针前进行非空判断。
耦合度较强的模块互相调用时,一定要仔细考虑其调用关系,防止已经删除的对象被再次使用。
避免操作已发送消息的内存。
自动存储对象的地址不应赋值给其他的在第一个对象已经停止存在后仍然保持的对象(具有更大作用域的对象或者静态对象或者从一个函数返回的对象)
规则6.4 编程时,要防止差1错误。
说明:此类错误一般是由于把“<=”误写成“<”或“>=”误写成“>”等造成的,由此引起的后果,很多情况下是很严重的,所以编程时,一定要在这些地方小心。当编完程序后,应对这些操作符进行彻底检查。使用变量时要注意其边界值的情况。
建议6.1 函数中分配的内存,在函数退出之前要释放。
说明:有很多函数申请内存,,保存在数据结构中,要在申请处加上注释,说明在何处释放。
建议6.2 if语句尽量加上else分支,对没有else分支的语句要小心对待。
建议6.3 不要滥用goto语句。
说明:goto语句会破坏程序的结构性,所以除非确实需要,最好不使用goto语句。
建议6.4 时刻注意表达式是否会上溢、下溢。
原则7.1 在保证软件系统的正确性、简洁、可维护性、可靠性及可测性的前提下,提高代码效率。
原则7.2 通过对数据结构、程序算法的优化来提高效率。
建议7.1 将不变条件的计算移到循环体外。
建议7.2 对于多维大数组,避免来回跳跃式访问数组成员。
建议7.3 创建资源库,以减少分配对象的开销。
建议7.4 将多次被调用的 “小函数”改为inline函数或者宏实现。
原则8.1 优秀的代码可以自我解释,不通过注释即可轻易读懂。
说明:优秀的代码不写注释也可轻易读懂,注释无法把糟糕的代码变好,需要很多注释来解释的代码往往存在坏味道,需要重构。
原则8.2 注释的内容要清楚、明了,含义准确,防止注释二义性。
原则8.3 在代码的功能、意图层次上进行注释,即注释解释代码难以直接表达的意图,而不是重复描述代码。
规则8.1 修改代码时,维护代码周边的所有注释,以保证注释与代码的一致性。不再有用的注释要删除。
规则8.2 文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者姓名、工号、内容、功能说明、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明。
规则8.3 函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、设计约束等。
规则8.4 全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明。
规则8.5 注释应放在其代码上方相邻位置或右方,不可放在下面。如放于上方则需与其上面的代码用空行隔开,且与下方代码缩进相同。
规则8.6 对于switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释。
规则8.7 避免在注释中使用缩写,除非是业界通用或子系统内标准化的缩写。
规则8.8 同一产品或项目组统一注释风格。
建议8.1 避免在一行代码或表达式的中间插入注释。
建议8.2 注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达。对于有外籍员工的,由产品确定注释语言。
建议8.3 文件头、函数头、全局常量变量、类型定义的注释格式采用工具可识别的格式。
说明:采用工具可识别的注释格式,例如doxygen格式,方便工具导出注释形成帮助文档。
规则9.1 程序块采用缩进风格编写,每级缩进为4个空格。
说明:当前各种编辑器/IDE都支持TAB键自动转空格输入,需要打开相关功能并设置相关功能。编辑器/IDE如果有显示TAB的功能也应该打开,方便及时纠正输入错误。
规则9.2 相对独立的程序块之间、变量说明之后必须加空行。
规则9.3 一条语句不能过长,如不能拆分需要分行写。一行到底多少字符换行比较合适,产品可以自行确定。
换行时有如下建议:
换行时要增加一级缩进,使代码可读性更好;
低优先级操作符处划分新行;换行时操作符应该也放下来,放在新行首;
换行时建议一个完整的语句放在一行,不要根据字符数断行
规则9.4 多个短语句(包括赋值语句)不允许写在同一行内,即一行只写一条语句。
规则9.5 if、for、do、while、case、switch、default等语句独占一行。
规则9.6 在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格;进行非对等操作时,如果是关系密切的立即操作符(如->),后不应加空格。
建议9.1 注释符(包括„/‟„//‟„/‟)与注释内容之间要用一个空格进行分隔。
建议9.2 源程序中关系较为紧密的代码应尽可能相邻。
规则10.1 表达式的值在标准所允许的任何运算次序下都应该是相同的。
建议10.1 函数调用不要作为另一个函数的参数使用,否则对于代码的调试、阅读都不利。
建议10.2 赋值语句不要写在if等语句中,或者作为函数的参数使用。
建议10.3 赋值操作符不能使用在产生布尔值的表达式上。
规则11.1 使用编译器的最高告警级别,理解所有的告警,通过修改代码而不是降低告警级别来消除所有告警。
规则11.2 在产品软件(项目组)中,要统一编译开关、静态检查选项以及相应告警清除策略。
规则11.3 本地构建工具(如PC-Lint)的配置应该和持续集成的一致。
规则11.4 使用版本控制(配置管理)系统,及时签入通过本地构建的代码,确保签入的代码不会影响构建成功。
建议11.1 要小心地使用编辑器提供的块拷贝功能编程。
原则12.1 模块划分清晰,接口明确,耦合性小,有明确输入和输出,否则单元测试实施困难。
说明:单元测试实施依赖于:
模块间的接口定义清楚、完整、稳定;
模块功能的有明确的验收条件(包括:预置条件、输入和预期结果);
模块内部的关键状态和关键数据可以查询,可以修改;
模块原子功能的入口唯一;
模块原子功能的出口唯一;
依赖集中处理:和模块相关的全局变量尽量的少,或者采用某种封装形式。
规则12.1 在同一项目组或产品组内,要有一套统一的为集成测试与系统联调准备的调测开关及相应打印函数,并且要有详细的说明。
规则12.2 在同一项目组或产品组内,调测打印的日志要有统一的规定。
说明:统一的调测日志记录便于集成测试,具体包括:
统一的日志分类以及日志级别;
通过命令行、网管等方式可以配置和改变日志输出的内容和格式;
在关键分支要记录日志,日志建议不要记录在原子函数中,否则难以定位;
调试日志记录的内容需要包括文件名/模块名、代码行号、函数名、被调用函数名、错误码、错误发生的环境等。
规则12.3 使用断言记录内部假设。
规则12.4 不能用断言来检查运行时错误。
说明:断言是用来处理内部编程或设计是否符合假设;不能处理对于可能会发生的且必须处理的情况要写防错程序,而不是断言。如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现。
建议12.1 为单元测试和系统故障注入测试准备好方法和通道。
原则13.1 对用户输入进行检查。
说明:不能假定用户输入都是合法的,因为难以保证不存在恶意用户,即使是合法用户也可能由于误用误操作而产生非法输入。用户输入通常需要经过检验以保证安全,特别是以下场景:
用户输入作为循环条件
用户输入作为数组下标
用户输入作为内存分配的尺寸参数
用户输入作为格式化字符串
用户输入作为业务数据(如作为命令执行参数、拼装sql语句、以特定格式持久化)
这些情况下如果不对用户数据做合法性验证,很可能导致DOS、内存越界、格式化字符串漏洞、命令注入、SQL注入、缓冲区溢出、数据破坏等问题。
可采取以下措施对用户输入检查:
用户输入作为数值的,做数值范围检查
用户输入是字符串的,检查字符串长度
用户输入作为格式化字符串的,检查关键字“%”
用户输入作为业务数据,对关键字进行检查、转义
规则13.1 确保所有字符串是以NULL结束。
说明: C语言中‟\0‟作为字符串的结束符,即NULL结束符。标准字符串处理函数(如strcpy()、 strlen())
依赖NULL结束符来确定字符串的长度。没有正确使用NULL结束字符串会导致缓冲区溢出和其它未定义的行为。
为了避免缓冲区溢出,常常会用相对安全的限制字符数量的字符串操作函数代替一些危险函数。如:
用strncpy()代替strcpy()
用strncat()代替strcat()
用snprintf()代替sprintf()
用fgets()代替gets()
这些函数会截断超出指定限制的字符串,但是要注意它们并不能保证目标字符串总是以NULL结尾。如果源字符串的前n个字符中不存在NULL字符,目标字符串就不是以NULL结尾。
规则13.2 不要将边界不明确的字符串写到固定长度的数组中。
说明:边界不明确的字符串(如来自gets()、getenv()、scanf()的字符串),长度可能大于目标数组长度,直接拷贝到固定长度的数组中容易导致缓冲区溢出。
规则13.3 避免整数溢出。
说明:当一个整数被增加超过其最大值时会发生整数上溢,被减小小于其最小值时会发生整数下溢。带符号和无符号的数都有可能发生溢出。
规则13.4 避免符号错误。
说明:有时从带符号整型转换到无符号整型会发生符号错误,符号错误并不丢失数据,但数据失去了原来的含义。
带符号整型转换到无符号整型,最高位(high-order bit)会丧失其作为符号位的功能。如果该带符号整数的值非负,那么转换后值不变;如果该带符号整数的值为负,那么转换后的结果通常是一个非常大的正数。
规则13.5:避免截断错误。
说明:将一个较大整型转换为较小整型,并且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留而高位被丢弃。截断错误会引起数据丢失。使用截断后的变量进行内存操作,很可能会引发问题。
规则13.6:确保格式字符和参数匹配。
说明:使用格式化字符串应该小心,确保格式字符和参数之间的匹配,保留数量和数据类型。格式字符和参数之间的不匹配会导致未定义的行为。大多数情况下,不正确的格式化字符串会导致程序异常终止。
规则13.7 避免将用户输入作为格式化字符串的一部分或者全部。
说明:调用格式化I/O函数时,不要直接或者间接将用户输入作为格式化字符串的一部分或者全部。攻击者对一个格式化字符串拥有部分或完全控制,存在以下风险:进程崩溃、查看栈的内容、改写内存、甚至执行任意代码。
规则13.8 避免使用strlen()计算二进制数据的长度。
说明:strlen()函数用于计算字符串的长度,它返回字符串中第一个NULL结束符之前的字符的数量。因此用strlen()处理文件I/O函数读取的内容时要小心,因为这些内容可能是二进制也可能是文本。
规则13.9 使用int类型变量来接受字符I/O函数的返回值。
规则13.10 防止命令注入。
说明:C99函数system()通过调用一个系统定义的命令解析器(如UNIX的shell,Windows的CMD.exe)来执行一个指定的程序/命令。类似的还有POSIX的函数popen()。
规则14.1 在编写代码的同时,或者编写代码前,编写单元测试用例验证软件设计/编码的正确。
建议14.1 单元测试关注单元的行为而不是实现,避免针对函数的测试。
说明:应该将被测单元看做一个被测的整体,根据实际资源、进度和质量风险,权衡代码覆盖、打桩工作量、补充测试用例的难度、被测对象的稳定程度等,一般情况下建议关注模块/组件的测试,尽量避免针对函数的测试。尽管有时候单个用例只能专注于对某个具体函数的测试,但我们关注的应该是函数的行为而不是其具体实现细节。
规则15.1 不能定义、重定义或取消定义标准库/平台中保留的标识符、宏和函数。
建议15.1 不使用与硬件或操作系统关系很大的语句,而使用建议的标准语句,以提高软件的可移植性和可重用性。
建议15.2 除非为了满足特殊需求,避免使用嵌入式汇编。
说明:程序中嵌入式汇编,一般都对可移植性有较大的影响。
MySQL
Oracle(除了政府项目,一般用不到)
Sqlite(客户端项目比较好用)
redis
ElesticSearch
Percona Toolkit 工具包是一组高级的管理 MySQL 的工具包集,可以用来执行各种通过手工执行非常复杂和麻烦的系统任务。简称 PT 工具,由 Percona 公司开发维护,是广大数据库维护人员的好帮手。
Skywalking是一个分布式追踪系统,可以跟踪整个分布式系统的请求流程,并记录每个组件之间的调用关系和时间消耗。Skywalking被广泛应用于微服务架构中,帮助用户快速定位分布式事务链路上的问题。 Skywalking比较适合跟踪分布式事务链路
Prometheus是一个开源监控系统,可以收集并存储各种指标数据,并提供强大的查询语言和可视化界面。Prometheus被广泛应用于分布式系统监控、服务质量保障等方面。 Prometheus比较适合收集指标数据并进行分析
可能需要坐飞机,私有仓库,是需要付费的
国内平台,企业版支持5人组队开发
可实现完全私有化的git管理,私密性更强
国内镜像源:阿里云、网易云、docker官方 访问源站可能需要翻墙
阿里云镜像源,可构建 maven, docker, node 等私有仓库
是为企业用户设计的容器镜像仓库开源项目,包括了权限管理(RBAC)、LDAP、审计、安全漏洞扫描、镜像验真、管理界面、自我注册、HA 等企业必需的功能,同时针对中国用户的特点,设计镜像复制和中文支持等功能。 我之前用过的是使用其docker仓库功能,部署在服务器上,可实现私有仓库
Nexus 是一个私有 Maven 仓库管理器,主要用于公司内部,用于搭建私服,可实现镜像管理 也提供yum、pypi、npm、docker、nuget、rubygems 等私有仓库
Arthas是一个Java诊断工具,可以实时查看应用程序的运行状态、调用堆栈、方法耗时等信息,并进行动态修改代码或者配置。Arthas被广泛应用于线上故障排查和性能优化中,同时也支持离线日志分析。 Arthas比较适合快速排查线上问题
工具类优先使用Spring自带的(稳定、基本没bug)。Spring自带的工具基本都够用,非必要不要用其他乱七八糟的工具类(不稳定、bug多)。
JSON工具
HTTP客户端
基于Reactor模型能能更优,如果是新项目推荐使用
Spring早期的http客户端,是使用阻塞线程模型
String工具
Spring自带,稳定性好,能满足大多数场景,目前已知的唯一痛点是不支持 equals