通讯中的编码问题

通讯中的编码问题

这里的LIS接口通讯主要指的是串口(RS232)/网口(TCP或UDP)通讯,而串口或网口通讯中,我们经常会遇到乱码或结果解析错误的问题,那么我们应该怎样做才能避免这些问题呢?

首先我们要知道,想要开发接口特别是双工接口,首先要能读懂接口的通讯文档,而通讯文档中经常会有所使用通讯协议的介绍,流入ASTM、HL7。

而通讯协议一般都制定了编码字符集,比如ASCIIUTF-8GBK等,虽然字符编码的概念是基础,我们都能理解,但是也不能确保顺利的开发出接口,我们还需要了解HEX、控制字符、校验等。

字符编码

首先,字符编码(Character encoding)也称字集码,是把字符集中的字符编码为指定集合中某一对象(例如:比特模式、自然数序列、8位组或者电脉冲),以便文本在计算机中存储和通过通信网络的传递。

常见的例子包括将拉丁字母表编码成摩斯电码和ASCII。其中,ASCII将字母、数字和其它符号编号,并用7比特的二进制来表示这个整数。通常会额外使用一个扩充的比特,以便于以1个字节的方式存储。

ASCII码使用指定的7位或8位二进制数组合来表示128或256种可能的字符,包括大小写字母、数字0-9、标点符号、非打印字符(换行符、制表符等4个)以及控制字符(退格、响铃等)组成。

但是,由于它是针对英语设计的,当处理带有音调标号(形如汉语的拼音)的亚洲文字时就会出现问题。

所以实际我们在通讯过程中会遇到另外几种支持更多字符的编码,例如UTF-8GBK(或GB2312GB2312GBK的子集)。

控制字符

ASCII编码中0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符),如控制符:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(响铃)等;通信专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等;ASCII值为8、9、10和13分别转换为退格、制表、换行和回车字符。它们并没有特定的图形显示,但会依不同的应用程序,而对文本显示有不同的影响。

ASCII编码标准表:

Bin
(二进制)
Oct
(八进制)
Dec
(十进制)
Hex
(十六进制)
缩写/字符 解释
0000 0000 00 0 0x00 NUL(null) 空字符
0000 0001 01 1 0x01 SOH(start of headline) 标题开始
0000 0010 02 2 0x02 STX (start of text) 正文开始
0000 0011 03 3 0x03 ETX (end of text) 正文结束
0000 0100 04 4 0x04 EOT (end of transmission) 传输结束
0000 0101 05 5 0x05 ENQ (enquiry) 请求
0000 0110 06 6 0x06 ACK (acknowledge) 收到通知
0000 0111 07 7 0x07 BEL (bell) 响铃
0000 1000 010 8 0x08 BS (backspace) 退格
0000 1001 011 9 0x09 HT (horizontal tab) 水平制表符
0000 1010 012 10 0x0A LF (NL line feed, new line) 换行键
0000 1011 013 11 0x0B VT (vertical tab) 垂直制表符
0000 1100 014 12 0x0C FF (NP form feed, new page) 换页键
0000 1101 015 13 0x0D CR (carriage return) 回车键
0000 1110 016 14 0x0E SO (shift out) 不用切换
0000 1111 017 15 0x0F SI (shift in) 启用切换
0001 0000 020 16 0x10 DLE (data link escape) 数据链路转义
0001 0001 021 17 0x11 DC1 (device control 1) 设备控制1
0001 0010 022 18 0x12 DC2 (device control 2) 设备控制2
0001 0011 023 19 0x13 DC3 (device control 3) 设备控制3
0001 0100 024 20 0x14 DC4 (device control 4) 设备控制4
0001 0101 025 21 0x15 NAK (negative acknowledge) 拒绝接收
0001 0110 026 22 0x16 SYN (synchronous idle) 同步空闲
0001 0111 027 23 0x17 ETB (end of trans. block) 结束传输块
0001 1000 030 24 0x18 CAN (cancel) 取消
0001 1001 031 25 0x19 EM (end of medium) 媒介结束
0001 1010 032 26 0x1A SUB (substitute) 代替
0001 1011 033 27 0x1B ESC (escape) 换码(溢出)
0001 1100 034 28 0x1C FS (file separator) 文件分隔符
0001 1101 035 29 0x1D GS (group separator) 分组符
0001 1110 036 30 0x1E RS (record separator) 记录分隔符
0001 1111 037 31 0x1F US (unit separator) 单元分隔符
0010 0000 040 32 0x20 (space) 空格
0010 0001 041 33 0x21 ! 叹号
0010 0010 042 34 0x22 双引号
0010 0011 043 35 0x23 # 井号
0010 0100 044 36 0x24 $ 美元符
0010 0101 045 37 0x25 % 百分号
0010 0110 046 38 0x26 & 和号
0010 0111 047 39 0x27 闭单引号
0010 1000 050 40 0x28 ( 开括号
0010 1001 051 41 0x29 ) 闭括号
0010 1010 052 42 0x2A * 星号
0010 1011 053 43 0x2B + 加号
0010 1100 054 44 0x2C , 逗号
0010 1101 055 45 0x2D - 减号/破折号
0010 1110 056 46 0x2E . 句号
0010 1111 057 47 0x2F / 斜杠
0011 0000 060 48 0x30 0 字符0
0011 0001 061 49 0x31 1 字符1
0011 0010 062 50 0x32 2 字符2
0011 0011 063 51 0x33 3 字符3
0011 0100 064 52 0x34 4 字符4
0011 0101 065 53 0x35 5 字符5
0011 0110 066 54 0x36 6 字符6
0011 0111 067 55 0x37 7 字符7
0011 1000 070 56 0x38 8 字符8
0011 1001 071 57 0x39 9 字符9
0011 1010 072 58 0x3A : 冒号
0011 1011 073 59 0x3B ; 分号
0011 1100 074 60 0x3C < 小于
0011 1101 075 61 0x3D = 等号
0011 1110 076 62 0x3E > 大于
0011 1111 077 63 0x3F ? 问号
0100 0000 0100 64 0x40 @ 电子邮件符号
0100 0001 0101 65 0x41 A 大写字母A
0100 0010 0102 66 0x42 B 大写字母B
0100 0011 0103 67 0x43 C 大写字母C
0100 0100 0104 68 0x44 D 大写字母D
0100 0101 0105 69 0x45 E 大写字母E
0100 0110 0106 70 0x46 F 大写字母F
0100 0111 0107 71 0x47 G 大写字母G
0100 1000 0110 72 0x48 H 大写字母H
0100 1001 0111 73 0x49 I 大写字母I
0100 1010 0112 74 0x4A J 大写字母J
0100 1011 0113 75 0x4B K 大写字母K
0100 1100 0114 76 0x4C L 大写字母L
0100 1101 0115 77 0x4D M 大写字母M
0100 1110 0116 78 0x4E N 大写字母N
0100 1111 0117 79 0x4F O 大写字母O
0101 0000 0120 80 0x50 P 大写字母P
0101 0001 0121 81 0x51 Q 大写字母Q
0101 0010 0122 82 0x52 R 大写字母R
0101 0011 0123 83 0x53 S 大写字母S
0101 0100 0124 84 0x54 T 大写字母T
0101 0101 0125 85 0x55 U 大写字母U
0101 0110 0126 86 0x56 V 大写字母V
0101 0111 0127 87 0x57 W 大写字母W
0101 1000 0130 88 0x58 X 大写字母X
0101 1001 0131 89 0x59 Y 大写字母Y
0101 1010 0132 90 0x5A Z 大写字母Z
0101 1011 0133 91 0x5B [ 开方括号
0101 1100 0134 92 0x5C \ 反斜杠
0101 1101 0135 93 0x5D ] 闭方括号
0101 1110 0136 94 0x5E ^ 脱字符
0101 1111 0137 95 0x5F _ 下划线
0110 0000 0140 96 0x60 ` 开单引号
0110 0001 0141 97 0x61 a 小写字母a
0110 0010 0142 98 0x62 b 小写字母b
0110 0011 0143 99 0x63 c 小写字母c
0110 0100 0144 100 0x64 d 小写字母d
0110 0101 0145 101 0x65 e 小写字母e
0110 0110 0146 102 0x66 f 小写字母f
0110 0111 0147 103 0x67 g 小写字母g
0110 1000 0150 104 0x68 h 小写字母h
0110 1001 0151 105 0x69 i 小写字母i
0110 1010 0152 106 0x6A j 小写字母j
0110 1011 0153 107 0x6B k 小写字母k
0110 1100 0154 108 0x6C l 小写字母l
0110 1101 0155 109 0x6D m 小写字母m
0110 1110 0156 110 0x6E n 小写字母n
0110 1111 0157 111 0x6F o 小写字母o
0111 0000 0160 112 0x70 p 小写字母p
0111 0001 0161 113 0x71 q 小写字母q
0111 0010 0162 114 0x72 r 小写字母r
0111 0011 0163 115 0x73 s 小写字母s
0111 0100 0164 116 0x74 t 小写字母t
0111 0101 0165 117 0x75 u 小写字母u
0111 0110 0166 118 0x76 v 小写字母v
0111 0111 0167 119 0x77 w 小写字母w
0111 1000 0170 120 0x78 x 小写字母x
0111 1001 0171 121 0x79 y 小写字母y
0111 1010 0172 122 0x7A z 小写字母z
0111 1011 0173 123 0x7B { 开花括号
0111 1100 0174 124 0x7C
0111 1101 0175 125 0x7D } 闭花括号
0111 1110 0176 126 0x7E ~ 波浪号
0111 1111 0177 127 0x7F DEL (delete) 删除

当然我们可以使用简单的几行代码将这些字符打印出来,因为NUL字符在Windows系统剪贴板中无法复制,而且会截断后面的内容,这里直接输出到文件中。

这里我们就不创建项目了,如果是执行一些简单的代码查看运行效果,例如生成一个GUID,这里建议使用C#交互窗口,可以从 VS工具栏 –> 视图 –> 其他窗口 –> C#交互窗口 打开。

运行以下代码:

hex1

运行成功后会在桌面输出一个文件hex.txt

注意,如果存在控制字符的文件,建议不要使用系统自带的记事本文件查看,推荐使用Notepad++查看,具体差异可以通过下图看出:

hex2

首先是换行符、回车符,Notepad++中默认就是Unix(LF)格式,而记事本要到Win10才能支持,其次控制字符,在Notepad++中,除了DEL,其他都可以看出其控制字符的内容,而记事本就什么也看不到了。另外打开了Notepad++的“Show all characters”,我们甚至可以知道一段空白究竟是由空格符还是制表符构成的,换行效果是由于回车符还是换行符导致的。

当然,如果我们在程序中需要使用这些字符除了上面展示的char.ConvertFromUtf32()方法外,还可以使用C#中的转义符号,如下给出的是常用的控制字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
public static class ControlCharacter
{
/// <summary>
/// 空字符
/// </summary>
public const string NUL = "\u0000";
/// <summary>
/// 标题开始
/// </summary>
public const string SOH = "\u0001";
/// <summary>
/// 正文开始
/// </summary>
public const string STX = "\u0002";
/// <summary>
/// 正文结束
/// </summary>
public const string ETX = "\u0003";
/// <summary>
/// 传输结束
/// </summary>
public const string EOT = "\u0004";
/// <summary>
/// 请求
/// </summary>
public const string ENQ = "\u0005";
/// <summary>
/// 收到通知
/// </summary>
public const string ACK = "\u0006";
/// <summary>
/// 响铃
/// </summary>
public const string BEL = "\u0007";
/// <summary>
/// 退格
/// </summary>
public const string BS = "\u0008";
/// <summary>
/// 水平制表符
/// </summary>
public const string HT = "\u0009";
/// <summary>
/// 换行键
/// </summary>
public const string LF = "\u000a";
/// <summary>
/// 垂直制表符
/// </summary>
public const string VT = "\u000b";
/// <summary>
/// 换页键
/// </summary>
public const string FF = "\u000c";
/// <summary>
/// 回车键
/// </summary>
public const string CR = "\u000d";
/// <summary>
/// 不用切换
/// </summary>
public const string SO = "\u000e";
/// <summary>
/// 启用切换
/// </summary>
public const string SI = "\u000f";
/// <summary>
/// 数据链路转义
/// </summary>
public const string DLE = "\u0010";
/// <summary>
/// 设备控制1
/// </summary>
public const string DC1 = "\u0011";
/// <summary>
/// 设备控制2
/// </summary>
public const string DC2 = "\u0012";
/// <summary>
/// 设备控制3
/// </summary>
public const string DC3 = "\u0013";
/// <summary>
/// 设备控制4
/// </summary>
public const string DC4 = "\u0014";
/// <summary>
/// 拒绝接收
/// </summary>
public const string NAK = "\u0015";
/// <summary>
/// 同步空闲
/// </summary>
public const string SYN = "\u0016";
/// <summary>
/// 结束传输块
/// </summary>
public const string ETB = "\u0017";
/// <summary>
/// 取消
/// </summary>
public const string CAN = "\u0018";
/// <summary>
/// 媒介结束
/// </summary>
public const string EM = "\u0019";
/// <summary>
/// 代替
/// </summary>
public const string SUB = "\u001a";
/// <summary>
/// 换码(溢出)
/// </summary>
public const string ESC = "\u001b";
/// <summary>
/// 文件分隔符
/// </summary>
public const string FS = "\u001c";
/// <summary>
/// 分组符
/// </summary>
public const string GS = "\u001d";
/// <summary>
/// 记录分隔符
/// </summary>
public const string RS = "\u001e";
/// <summary>
/// 单元分隔符
/// </summary>
public const string US = "\u001f";
/// <summary>
/// 删除
/// </summary>
public const string DEL = "\u007f";
}

十六进制

十六进制(简写为hex或下标16)在数学中是一种逢16进1的进位制。一般用数字0到9和字母A到F(或af)表示,其中:AF表示10~15,这些称作十六进制数字。

而数据接收串口与网口通讯时,如果我们无法确认接收的编码格式,可以尝试使用十六进制接收,然后根据十六进制数据来判断接收数据的编码格式。

而在发送时,如果我们传输的是控制字符,我们无法使用输入法将控制字符输入到发送区,这时我们可以通过十六进制发送,来发送控制字符,例如需要发送ACK,则只需要输入06即可。

hex3

在通讯数据的接收中,我们常常需要将十六进制数据进行转换,其中包括原始文本、十六进制、转义消息三者的相互转换,十六进制与原始文本我们可以理解,转义文本的意思是将不可见的控制字符转换为另外一种特殊的格式进行显示。

例如:收到通知<ACK>的十六进制是06,转义文本是#06'。回车符<CR>的十六进制是0D,转义文本为#0D'

这里开发了一个简单的转换工具供参考:

hex4

源码:https://github.com/hd2y/HexConverter

串口通讯调试与开发

工作中,LIS系统经常需要开发接口与仪器或流水线对接,串口通讯应该是最常见的几种通讯方式之一了。

至于串口是什么、串口通讯又是什么,这些基本概念就不多做介绍了。

这里就针对于我在串口开发中碰到过的一些问题,做一个简单的分享。

串口模拟

无论是开发还是测试,肯定都离不开串口的模拟测试,我们不大可能直接在电脑上安装两个串口,所以模拟串口就是最优解了,从网上可以很轻松的搜索到模拟串口的软件。

这里我建议使用Virtual Serial Port Driver,大家可以搜索VSPD下载:

serialport0

安装什么的就不过多解释了,安装完成后我们可以打开并添加模拟串口或者删除模拟的串口,界面如下:

serialport1

如果对应的串口被使用,也可以从这里看出其串口的状态:

serialport2

当然我们可以在我们的电脑中查看现有的串口,设备管理器中可以查看:

serialport3

串口调试工具

串口调试工具就是用于串口数据接收/发送的工具软件,比较简单易用的是“串口调试助手V2.2”。

serialport4

但是使用中有几点需要注意:

  1. 偶尔会发生崩溃,这是串口调试工具的问题;
  2. 只能使用COM1-COM4这几个串口,有局限性;
  3. 与某些需要检测串口监听状态的仪器连接,虽然已经打开串口但是可能仪器仍然提示连接失败;

如果碰到以上问题,那就只能只能自己找解决方案,另外如果具备一定开发经验后,建议根据自身需求来定制自己的串口调试工具,可以避免以上问题。

串口监视精灵

除了串口调试助手以外,另外比较推荐的工具就是串口监视精灵了,顾名思义其就是类似于网口通讯中的抓包工具。

当初从网上找到这个工具的原因是之前LIS系统替换第三方LIS系统,有一个仪器的双工接口开发一直存在问题,所以想看一下第三方LIS怎么实现的双工通讯消息的交互。

当然如果开发的串口通讯工具中没有输出日志,也可以使用该工具进行监听。

一般我们网上能够找到的分为无DLL版和安装版,这里建议使用安装版。

serialport5

上图为无Dll版,需要选择监听串口所在的进程,中文会乱码,能监听到串口的关闭但是打开的事件则没有输出。

serialport6

上图是安装版,使用安装版需要注意如果系统是x64,第一次使用需要点击软件界面上的“启用64位系统驱动签名”,另外重启。如果监听不到数据,尝试先关闭通讯软件,打开监听后再打开通讯软件。

安装版能够监听到的数据就更完整一些,且每次数据的收发都统一显示为一条记录,这就比无Dll版本的更加友好,但是同样存在的问题是因为是ASC编码所以中文仍然是乱码,需要自己使用HEX编码进行转换。

另外安装版也附带有一个串口调试工具可以使用:

serialport7

串口通讯开发

获取当前电脑的串口

引入System.IO.Ports命名空间,然后可以使用SerialPort.GetPortNames()获取当前电脑的所有串行端口名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.IO.Ports;

namespace SerialPortTestConsole
{
class Program
{
static void Main(string[] args)
{
string[] ports = SerialPort.GetPortNames();
foreach (string port in ports)
{
Console.WriteLine(port);
}
Console.ReadKey();
}
}
}

串口数据收发

引入System.IO.Ports命名空间,然后实例化SerialPort对象,使用Open方法打开串口,DataReceived事件进行数据接收,Write方法可以进行数据发送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
using System;
using System.IO.Ports;
using System.Text;

namespace SerialPortTestConsole
{
class Program
{
static void Main(string[] args)
{
//注意:以下为串口初始化常用的属性,赋值为默认值,如果没有赋值属性默认也是这些值
SerialPort port = new SerialPort()
{
PortName = "COM1",//端口号
BaudRate = 9600,//波特率
DataBits = 8,//数据位
Parity = Parity.None,//校验位
StopBits = StopBits.One,//停止位
Encoding = Encoding.ASCII,//文本转换编码
WriteBufferSize = 2048,//输出缓存
ReadBufferSize = 4096,//输入缓存
WriteTimeout = -1,//写超时
ReadTimeout = -1,//读超时
RtsEnable = false,//RTS信号:建议启用
DtrEnable = false,//DTR信号:建议启用
};

//数据接收事件 输出到控制台
port.DataReceived += new SerialDataReceivedEventHandler((sender, e) =>
{
while (port.BytesToRead > 0)
{
int length = port.BytesToRead;
byte[] buffer = new byte[length];
port.Read(buffer, 0, length);
Console.WriteLine(port.Encoding.GetString(buffer));
}
});

//打开串口
if (!port.IsOpen)
{
try
{
port.Open();
Console.WriteLine($"串口打开成功:{port.PortName}");
}
catch (Exception exception)
{
Console.WriteLine($"打开串口失败:{exception.Message}");
}
}


if (port.IsOpen)
{
//输入串口发送内容
string input = "";
do
{
Console.WriteLine("请输入要发送的内容:");
input = Console.ReadLine();
if (!string.IsNullOrEmpty(input))
{
byte[] buffer = port.Encoding.GetBytes(input);
port.Write(buffer, 0, buffer.Length);
}
} while (!string.IsNullOrEmpty(input));

//关闭串口释放资源
port.Close();
}
port.Dispose();
}
}
}

一个简单的调试工具

通过以上例子基本上对串口数据手法有个简单的了解,这里我们设计一个工具,来实现串口调试工具的基本功能。

SerialPort的属性/方法/事件基本没有什么好说的了,这里主要需要了解HEX数据的转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/// <summary>
/// 字符串拓展方法
/// </summary>
public static partial class StringExtension
{
/// <summary>
/// 将指定字符串转换为十六进制Hex字符串形式。
/// </summary>
/// <param name="input">要转换的原始字符串。</param>
/// <param name="e">编码</param>
/// <returns>转换后的内容</returns>
public static string ToHex(this string input, Encoding e = null)
{
e = e ?? Encoding.UTF8;
byte[] byteArray = e.GetBytes(input);
return BitConverter.ToString(byteArray).Replace('-', ' ');
}

/// <summary>
/// 将十六进制Hex字符串转换为原始字符串。
/// </summary>
/// <param name="input">十六进制字符串内容</param>
/// <param name="e">编码</param>
/// <returns>原始字符串内容</returns>
public static string ReHex(this string input, Encoding e = null)
{
e = e ?? Encoding.UTF8;
input = Regex.Replace(input, "[^0-9A-F]", "");
if (input.Length <= 0) return "";
byte[] vBytes = new byte[input.Length / 2];
for (int i = 0; i < vBytes.Length; i++)
vBytes[i] = byte.Parse(input.Substring(i * 2, 2), NumberStyles.HexNumber);
return e.GetString(vBytes);
}

/// <summary>
/// 将十六进制Hex字符串转换为二进制数据。
/// </summary>
/// <param name="input">十六进制字符串内容</param>
/// <returns>原始字符串内容</returns>
public static byte[] ReHexToBuffer(this string input)
{
input = Regex.Replace(input, "[^0-9A-F]", "");
if (string.IsNullOrEmpty(input)) return new byte[0];
byte[] vBytes = new byte[input.Length / 2];
for (int i = 0; i < vBytes.Length; i++)
vBytes[i] = byte.Parse(input.Substring(i * 2, 2), NumberStyles.HexNumber);
return vBytes;
}
}

/// <summary>
/// 二进制数据拓展方法
/// </summary>
public static partial class BufferExtension
{
/// <summary>
/// 将指定二进制数据转换为十六进制Hex字符串形式。
/// </summary>
/// <param name="input">要转换的二进制数据。</param>
/// <returns>转换后的内容</returns>
public static string ToHex(this byte[] input)
{
return BitConverter.ToString(input).Replace('-', ' ');
}
}

然后就是简单实现后的效果:

serialport8

源代码下载:https://github.com/hd2y/SerialPortTest

如何修复损坏的SQLite数据库文件

最近处理过一个SQLite数据库因为未知原因损坏,无法进行数据操作的而导致软件无法正常使用的问题。

虽然工具软件是基于Code First开发,可以直接删除数据库来重新生成数据文件,但是因为里面存储了很多基本参数,运维人员认为重新设置这些参数比较麻烦,所以希望能够提供一个方案来修复已经损坏的数据库。

和运维沟通后将数据库文件导回,尝试跟踪问题。从网上查询到解决方案,这里简单做一个记录。

排查

使用SQLite数据库管理工具SQLite Studio进行数据查询操作,访问正常,尝试直接删除某一个表的数据出现如下错误提示:

1
[10:42:22] Error while executing SQL query on database 'data': database disk image is malformed

从网上查找如何排查错误,发现一个语句可以对SQLite数据库进行检测:

1
PRAGMA integrity_check;

以上语句是执行整个库的完全性检查,会查看错序的记录、丢失的页,毁坏的索引等。

执行后我获得的信息是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
"*** in database main ***
Main freelist: freelist leaf count too big on page 138339
Main freelist: invalid page number 218103808
On tree page 42 cell 176: 2nd reference to page 138339
Page 18 is never used
Page 20 is never used
Page 23 is never used
Page 25 is never used
Page 26 is never used
Page 27 is never used
Page 28 is never used
Page 29 is never used
Page 30 is never used
Page 31 is never used
Page 32 is never used
Page 33 is never used
Page 34 is never used
Page 35 is never used
Page 36 is never used
Page 37 is never used
Page 38 is never used
Page 39 is never used
Page 40 is never used
Page 41 is never used
Page 43 is never used
Page 44 is never used
Page 45 is never used
Page 46 is never used
Page 47 is never used
Page 48 is never used
Page 49 is never used
Page 50 is never used
Page 51 is never used
Page 52 is never used
Page 53 is never used
Page 54 is never used
Page 55 is never used
Page 56 is never used
Page 57 is never used
Page 58 is never used
Page 59 is never used
Page 60 is never used
Page 61 is never used
Page 62 is never used
Page 63 is never used
Page 64 is never used
Page 65 is never used
Page 66 is never used
Page 67 is never used
Page 68 is never used
Page 69 is never used
Page 70 is never used
Page 71 is never used
Page 73 is never used
Page 74 is never used
Page 75 is never used
Page 76 is never used
Page 77 is never used
Page 78 is never used
Page 79 is never used
Page 80 is never used
Page 81 is never used
Page 82 is never used
Page 83 is never used
Page 84 is never used
Page 85 is never used
Page 86 is never used
Page 88 is never used
Page 89 is never used
Page 90 is never used
Page 91 is never used
Page 92 is never used
Page 93 is never used
Page 94 is never used
Page 95 is never used
Page 96 is never used
Page 97 is never used
Page 98 is never used
Page 99 is never used
Page 100 is never used
Page 101 is never used
Page 102 is never used
Page 103 is never used
Page 104 is never used
Page 105 is never used
Page 106 is never used
Page 107 is never used
Page 108 is never used
Page 110 is never used
Page 111 is never used
Page 112 is never used
Page 113 is never used
Page 114 is never used
Page 115 is never used
Page 116 is never used
Page 117 is never used
Page 118 is never used
Page 119 is never used
Page 120 is never used
Page 121 is never used
Page 122 is never used"

后面的问题就简单了,从网上搜索这个错误可以获得解决方案,基本原理就是将数据库内容导出脚本,然后通过这个脚本重新构建数据库。

可以参考:https://techblog.dorogin.com/sqliteexception-database-disk-image-is-malformed-77e59d547c50

解决

首先我们需要下载SQLite数据库的命令行管理工具:sqlite3.exe,命令行工具如何使用可以参考:https://www.sqlite.org/cli.html

我们可以SQLite官网下载命令行工具,比如我下载的是sqlite-tools-win32-x86-3280000.zip,可以在这里找到:https://www.sqlite.org/download.html

解压文件,并将数据文件拷贝到同级目录。

执行导出:

  1. 双击sqlite3.exe运行SQLite数据库命令行工具。
  2. 执行.open data.db打开我们需要导出数据脚本的数据文件。
  3. 执行.mode insert将导出脚本模式修改为用于执行INSERT。
  4. 执行.output dump_all.sql指定导出文件的未知与文件名。
  5. 执行.dump执行导出。(需要注意的是如果数据库文件较大,可能需要耐心等待一段时间。)

如果内容较多,可能执行导出耗时会比较长,需要耐心等待。

执行完成后可以在文件夹下找到导出的脚本文件。

使用SQL文件生成数据库文件:

  1. 关闭原有的命令行工具,重新打开。
  2. 执行.open data.fixed.dbdata.fixed.db就是我们恢复后数据库的数据库名。
  3. 执行.read dump_all.sql将数据脚本加载到创建的数据库中,等待执行完成即完成数据库的修复。

执行成功会生成了一个数据库文件。

我们可以使用文章开篇所使用数据库管理工具尝试检索数据库的状态,如果反馈数据正常,则恢复工作完成。

1
PRAGMA integrity_check;

Web下打印体验最好的打印控件LODOP

什么是LODOP

LODOP是BS架构打印的解决方案,支持绝大多数浏览器,如果网站上有打印的需求,推荐使用该工具进行打印。

选择LODOP的原因不仅仅在于其对于web打印有着比原生打印更好的打印体验,主要是其还提供了丰富的API,让开发者可以访问打印任务的状态以及打印机的状态,设置打印机与打印输出的属性等等,让开发者可以通过这些实现更多的用户需求。

另外看了官网云打印,之前一直以为必须安装控件才能打印,所以应该是对于设备和系统有要求,但是目前来看也是不限制的,甚至可以使用移动端发送打印请求,但是我没有这方面的使用需求,这里仅仅讨论Windows平台下的打印使用经验。

官方网站:http://www.lodop.net/

LODOP安装使用

在官网里下载中心中我们可以下载到打印控件(产品下载)与JS调用打印控件的API(技术手册),并且产品下载的内容包含测试的样例。

因为官方给的例子中已经有详细的安装以及使用说明,所以这里不做赘述,可以进入下载中心下载解压后直接查看样例,样例1就是安装以及基本的使用说明,其他的样例是一些常见的使用场景。

强烈建议加入售后群,LODOP的售后人员回答问题都很专业且及时,有很好的售后体验。如果使用时发现了以上测试用例之外的使用场景,或者发现一些使用问题,基本都能在技术人员那里得到优质的反馈。

打印场景说明

使用LODOP打印应该也有两年多了,主要用于条码打印/检验报告单/报表,这里简单聊以下我的使用场景。

条码打印

因为系统架构是BS的,早期使用ScriptX进行打印主要有这么几个痛点:

  • 打印Html内容格式很难固定,需要设置条码打印机来控制纸张大小,而且这个过程非常繁琐,经常出现问题;
  • 不能跨浏览器,只能支持IE浏览器,而且一般需要开启兼容模式并添加受信任站点;
  • 如果需要设置打印场景的默认打印机,需要读取注册表来读取当前系统的打印机,这也是浏览器不兼容的一个主要问题;
  • 不弹出提示直接执行打印任务可能需要安装一些打印控件,但是这些打印控件安装繁琐,且部分系统支持有限;
  • 分页是个问题,内容一旦溢出,即使设置了打印纸张也没用;
  • 条码输出需要后端来支持,生成图片;

当然以上只是一部分,但是实际在没有LODOP前打印体验还更差,工程实施与运维在这里踩出过很多坑,而且很多问题会和打印环境也就是电脑系统/IE版本有关,出现问题也很难排查。

因为有打印条形码的需求,并且不想使用后端来生成条形码图片,所以这里我不推荐全部使用HTML设置打印内容。文本建议使用ADD_PRINT_TEXTADD_PRINT_TEXTA,区别是后者可以可以针对性的设置打印样式。条码输出建议使用ADD_PRINT_BARCODE

因为LODOP没有提供直接适用于浏览器的样式设置工具,所以建议自己配置一个模块维护样式。(样例中有使用设置工具生成模板的例子,但是不够相对来说不够直观)。

我这边的设计是BarcodeDesign(条码样式设计表)、BarcodeDesignDetail(条码样式设计明细表)、BarcodeDesignDetailStyle(条码样式设计明细样式表)。

BarcodeDesign是主表,控制打印的基本样式,维护基本的信息主要是样式名称、打印纸张大小、打印方向、样式说明等。

BarcodeDesignDetail是BarcodeDesign的从表,用于维护打印的具体内容,例如数据源、类型(条形码、文本、图片、图形、HTML内容、分页符等LODOP支持输出的格式)、顺序(结合类型中的分页符实现分页效果)等、内容的位置信息和宽高。

BarcodeDesignDetailStyle是BarcodeDesignDetail的从表,用于维护打印内容的样式信息,也就是SET_PRINT_STYLEA可以针对不同的内容类型设置的样式。

检验报告

检验报告单的打印,相对条码打印要复杂很多,因为基本样式是不固定的。公司在前期没有考虑用报表控件,开始是xsl输出,因为学习成本较高不利于维护,后面改用Html打印,除常见格式外,余下的每种报告单样式对应一个页面。

开始没有考虑FastReport是因为BS架构下兼容有问题,加上报告单的样式比较复杂,数据源比较复杂,分页/数据表设计/排版 在这些报表控件中很难设计出理想的效果,另外就是报表控件设计工具的使用对于我来说也有一定的学习成本,所以基于此干脆继续使用Html打印,而这恰好也是LODOP最擅长的。

设计主要就两个表:LabReportDesign(实验室检验报告单设计表)与LabReportDesignDetail(实验室检验报告单设计明细表)。

LabReportDesign是主表,主要用来配置报告单的基本信息,包括报告单名称、打印纸张信息、输出内容的宽度与边距、使用的字体大小与样式、是否使用特殊样式(默认走普通样式)、每页显示的数据行数等等。

LabReportDesignDetail是LabReportDesign的从表,主要用于配置一些基础的数据源信息,包括行头展示的病人与检验信息/页脚展示的病人与检验信息/报告单主题内容显示哪些结果信息/注脚显示的信息等。

这里如果是普通报告单,从以上两张表的设置,可以从后台拼接一份Html用于打印。

如果是特殊样式的报告单,这里也没有考虑使用页面,一方面不方便打印,另外就是考虑到检验医师签名后要将数据静态化到数据表中,供其他系统查阅,这里使用的Razor引擎解析指定目录下的cshtml文件,这里是可以返回一份Html内容的。

另外将Html内容静态化的另外一个好处就是,我们还可以使用一些工具例如wkhtmltopdf将文件生成PDF文件。

报表

普通报表打印输出问题不大,问题是一些报表的某些数据列,内容长度变化较大不方便分页,之前设计的打印都是每一页固定行数,就会出现某些内容打印超出纸张,某些内容只占了纸张的一半。

这里建议参考一下样例中数据表格分页打印的样例,但是需要注意的是表格数据内容较多时,打印控件进行数据装载的时间会比较长。

优缺点

优点:

  • BS架构实现CS架构下的打印体验,这一点是目前市面上打印控件做的最好的;
  • 浏览器兼容性较好,支持现代浏览器;
  • 样例完善,基本覆盖所有的打印场景;
  • 售后技术人员反馈回复及时,回复内容也比较专业;
  • 价格方面比较友好;

缺点:

  • 打印依赖IE浏览器解析,LODOP客户端或CLODOP服务端IE浏览器版本较低可能会出现一些问题;
  • 目前设计功能开发还不够,需要进一步完善;
  • 仅仅适用于打印样式比较简单的场景;
  • 如果需要将内容输出为PDF等格式文档文件可能需要依赖虚拟打印机等;

注意事项

  1. 打印的Html内容如果需要加载媒体资源例如图片,建议通过SET_PRINT_STYLEA设置HtmWaitMilSecs即超文本下载延迟,避免图片没有下载完成即执行打印;
  2. 尽量避免打印内容的媒体资源,例如图片404,否则可能出现任务响应缓慢;
  3. 打印Html内容一定要检查内容开闭标签是否有缺失,否则可能出现很奇怪的问题;
  4. 打印Html内容如果是外联样式,并且要使用该样式,注意打印的时候要加入到打印内容中;

利用Jint在C#中运行JS脚本并实现简单计算器

关于Jint

Jint是一个开源的JS脚本引擎,可以让我们在dotnet平台运行js代码,这使我们可以通过这一特性处理很多工作。

关于Jint的更多信息和用例可以参考:https://github.com/sebastienros/jint

Jint的用途

数学运算

在日常工作中存在一个需求:用户自己定义表达式,系统根据表达式替换表达式中项目的数值,计算生成另外一个项目的结果。

例如:[GLO] = [TP] - [ALB],已知[TP][ALB]的结果,需要我们求出[GLO]的结果。

这个表达式初看其实很简单,但是实际业务中,可能存在多层级的嵌套甚至次幂/开根号/自然对数等的运算,这样如果通过程序来解析表达式进行运算工作量比较大。

过去程序设计是在Web端触发特定事件后通过正则替换表达式中参与运算项目为实际数值,使用eval函数执行表达式,获取结果并保存到数据库。但是随着系统不断升级,仅仅依靠前端来更新结果,在某些情况下不够人性化而且不能满足需求。

检索关于算术运算,网上普遍推荐的方案是:

  1. 使用数据库拼接SQL语句;
  2. 通过C#反射实现类似js的eval函数效果;

但是这些方案的缺点都很明显:数据库方案需要保持数据库连接,消耗数据库性能,并且不能使用特殊的函数运算比如对数等求解;C#的反射数值中如果存在整数运算,则会取整,需要手动替换数值增加浮点型数字的标记比较麻烦。

几次碰壁以后,就在想,是不是可以在C#中运行js代码?在网上检索相关资料果然有戏。不过首先尝试的是Microsoft.JScript库,不过这个已经被微软标记为Obsolete,并且对于一些语法支持的不完善,所以这里不推荐。

首先我们来实现一个简单的计算器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/// <summary>
/// 获取算术式结果
/// </summary>
/// <param name="input">原始算术式</param>
public static void Calculator(string input = null)
{
do
{
//算式为空提示输入算式
if (string.IsNullOrEmpty(input))
{
Console.WriteLine("请输入一个算术式(退出输入Q):");
input = Console.ReadLine();
}

//非退出该方法
if (!string.Equals(input, "Q", StringComparison.OrdinalIgnoreCase))
{
try
{
//使用Jint执行算式
Engine engine = new Engine();
object result = engine.Execute(input).GetCompletionValue().ToObject();
Console.WriteLine($"算术式:{input} 输出:{result}");
}
catch (Exception exc)
{
Console.WriteLine($"算术式:{input} 运算出现异常:{exc.Message}");
}

//赋值空让用户循环输入
input = null;
}
} while (!string.Equals(input, "Q", StringComparison.OrdinalIgnoreCase));
}

测试一下效果,已经满足了我们的需求:

jint1

需要注意的是,JavaScript中对于浮点数的支持精度较低,建议运算后设定小数位数进行转换。

通过以上,我们进一步实现上面我们业务中需要的功能,下面是一个简单的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/// <summary>
/// 获取表达式结果
/// </summary>
/// <param name="input">原始表达式</param>
public static void GetResultFromExpression(string input = null)
{
string oldInput;
Regex regex = new Regex(@"\[([A-Za-z0-9]+)\]");//表达式中项目由字母和数字组成
do
{
//表达式为空提示输入表达式
if (string.IsNullOrEmpty(input))
{
Console.WriteLine("请输入表达式(退出输入Q):");
input = Console.ReadLine();
}

//用于输出原始表达式
oldInput = input;

//用于判断表达式项目是否已经替换
List<string> items = new List<string>();

//非退出该方法
if (!string.Equals(input, "Q", StringComparison.OrdinalIgnoreCase))
{
//匹配表达式中项目并替换
MatchCollection matches = regex.Matches(input);
foreach (Match match in matches)
{
//已经替换的跳过
if (items.Contains(match.Value))
{
continue;
}
items.Add(match.Value);
Console.WriteLine($"请为项目:{match.Value} 赋值:");
string item = Console.ReadLine();
input = input.Replace(match.Value, item);
}
try
{
//使用Jint执行表达式
Engine engine = new Engine();
object result = engine.Execute(input).GetCompletionValue().ToObject();
Console.WriteLine($"表达式:{oldInput} 算术式:{input} 输出:{result}");
}
catch (Exception exc)
{
Console.WriteLine($"表达式:{oldInput} 算术式:{input} 运算出现异常:{exc.Message}");
}

//赋值空让用户循环输入
input = null;
oldInput = null;
}
} while (!string.Equals(input, "Q", StringComparison.OrdinalIgnoreCase));
}

测试效果如下:

jint2

自定义脚本

系统中某些位置,可能需要比较大的自由度去处理业务数据,例如文档的输出,我们能从数据库中读取到结果是数值型,但是实施人员希望我们能处理成文本型,并且设定的条件较多,自由度很大。

当然最直接的方案就是我们将这些集成到业务代码中,友好一点的设置一个参数表,由工程人员维护。但是当这里的业务逻辑复杂或者存在很多的不定因素,这里的维护更新就变得困难。

而这个时候,我们可以通过写一些js脚本,来处理复杂逻辑的问题,例如文档中我们配置了一下内容:

[ItemName]的结果为[ResultValue],临床意义为:[Descript]。

[ItemName]显示的内容是“HIV”时,[ResultValue]的值小于1判定[Descript]为“阴性”,否则为待复查。若[ItemName]不是“HIV”时,[ResultValue]的值小于1判定[Descript]为“阴性”,否则为阳性。如果[ResultValue]中存在非数字部分,则只取数字部分内容。

这里如果用业务代码或者参数来写,可能复杂一些,因为像这样的业务逻辑还有很多,而且可能随时需要调整,但是我们可以将以上的内容稍稍进行调整,我们定义$#*[JS代码]*#来包括一段代码,将以上的内容进行调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[ItemName]的结果为[ResultValue],临床意义为:$#*
var n = '[ItemName]';
var r = '[ResultValue]'.replace(/[^0-9\+\-\.]/, '');
var d = '';
if (r !== '' && !isNaN(r)) {
if (n === 'HIV' && r >= 1) {
d = '待复查';
}
else if (r >= 1) {
d = '阳性';
} else {
d = '阴性';
}
}
d;*#。

那么我们写一个测试用例实现以下对以上内容的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/// <summary>
/// 执行自定义脚本获取内容
/// </summary>
/// <param name="input">脚本</param>
public static void ExecuteCustomScript(string input = null)
{
string oldInput;
Regex regex1 = new Regex(@"\[([A-Za-z0-9]+)\]");//脚本中项目由字母和数字组成
Regex regex2 = new Regex(@"\$#\*(.+)\*#", RegexOptions.Singleline);//匹配自定义脚本
do
{
//脚本为空提示输入
if (string.IsNullOrEmpty(input))
{
Console.WriteLine("请输入脚本(退出输入Q):");
input = Console.ReadLine();
}

//用于输出原始脚本
oldInput = input;

//用于判断脚本项目是否已经替换
List<string> items = new List<string>();

//用于判断脚本内容是否已经替换
List<string> scripts = new List<string>();

//非退出该方法
if (!string.Equals(input, "Q", StringComparison.OrdinalIgnoreCase))
{
//匹配脚本中项目并替换
{
MatchCollection matches = regex1.Matches(input);
foreach (Match match in matches)
{
//已经替换的跳过
if (items.Contains(match.Value))
{
continue;
}
items.Add(match.Value);
Console.WriteLine($"请为项目:{match.Value} 赋值:");
string item = Console.ReadLine();
input = input.Replace(match.Value, item);
}
}

//匹配内容中自定义脚本并执行替换
{
MatchCollection matches = regex2.Matches(input);
foreach (Match match in matches)
{
//已经替换的跳过
if (scripts.Contains(match.Value))
{
continue;
}

scripts.Add(match.Value);

try
{
//使用Jint执行内容中自定义脚本
Engine engine = new Engine();
object result = engine.Execute(match.Groups[1].Value).GetCompletionValue().ToObject();
input = input.Replace(match.Value, result.ToString());
}
catch (Exception exc)
{
Console.WriteLine($"脚本:{match.Groups[1].Value}\r\n执行出现异常:{exc.Message}");
}
}
}

Console.WriteLine($"**********************\r\n原始脚本:{oldInput}\r\n输出内容:{input}");

//还原公式让用户重新赋值
input = oldInput;
}
} while (!string.Equals(input, "Q", StringComparison.OrdinalIgnoreCase));
}

调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
JintTest.ExecuteCustomScript(@"[ItemName]的结果为[ResultValue],临床意义为:$#*
var n = '[ItemName]';
var r = '[ResultValue]'.replace(/[^0-9\+\-\.]/, '');
var d = '';
if (r !== '' && !isNaN(r)) {
if (n === 'HIV' && r >= 1) {
d = '待复查';
}
else if (r >= 1) {
d = '阳性';
} else {
d = '阴性';
}
}
d;*#。");

测试效果如下:

jint3

C#中实现简单爬虫项目

什么是爬虫

爬虫即网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。

我们日常使用的搜索引擎、比价网站等都是通过爬虫技术获取数据,而后经数据提取、分析、优化后供用户使用。当然日常生活中,我们也可以使用爬虫技术,获取我们感兴趣的网站进行数据解析分析,做出来一些有意思的功能或小工具,比如新闻提醒、事项通知、资料整理等。

这方面我们需要了解的技术主要是:Http请求、XPath、正则等。以下是一个简单的例子,用于获取京东商城的数据,例子仅供学习。

抓取京东所有商品类别

流程

  • 分析京东商品类别页面

    Chrome浏览器打开页面:https://www.jd.com/allsort.aspx

  • 查看页面源代码

    用XPath分析商品类别信息,包括分类ID、分类名、链接,XPath语法可参考:http://www.w3school.com.cn/xpath/xpath_syntax.asp

  • 获取数据

    使用 System.Net.WebRequest/System.Net.WebClient/HtmlAgilityPack.HtmlWeb 获取数据 (HtmlAgilityPack 可以通过 nuget 添加引用)

  • 解析数据

    使用 HtmlAgilityPack.HtmlDocument 解析包含商品类别信息的链接

实现

获取数据

使用 System.Net.WebRequest/System.Net.HttpWebRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
HtmlAgilityPack.HtmlDocument document = null;
WebRequest request = WebRequest.Create("https://www.jd.com/allsort.aspx");
using (WebResponse response = request.GetResponse())
{
using (Stream stream = response.GetResponseStream())
{
document = new HtmlAgilityPack.HtmlDocument();
document.Load(stream, Encoding.UTF8);
//MemoryStream memoryStream = (MemoryStream)stream;
//document.LoadHtml(Encoding.UTF8.GetString(memoryStream.ToArray()));
//memoryStream.Dispose();
}
}

使用 System.Net.Client

1
2
3
4
5
6
7
8
9
HtmlAgilityPack.HtmlDocument document = null;
using (WebClient client = new WebClient())
{
using (Stream stream = client.OpenRead("https://www.jd.com/allsort.aspx"))
{
document = new HtmlAgilityPack.HtmlDocument();
document.Load(stream, Encoding.UTF8);
}
}

使用 HtmlAgilityPack.HtmlWeb

1
2
3
4
5
HtmlWeb web = new HtmlWeb
{
OverrideEncoding = Encoding.UTF8
};
HtmlAgilityPack.HtmlDocument document = web.Load("https://www.jd.com/allsort.aspx");

解析数据

实体

1
2
3
4
5
6
7
public class Category
{
public int Id { get; set; }
public string JDId { get; set; }
public string Name { get; set; }
public string URL { get; set; }
}

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<Category> listCategory = new List<Category>();
if (document != null)
{
int iId = 1;//编号
Regex regex = new Regex("cat=(\\d+(\\,\\d+)*)");//匹配与提取类别ID正则
//取出所有链接
HtmlNodeCollection collection = document.DocumentNode.SelectNodes("//a[@href]");
foreach (var item in collection)
{
//正则匹配出是商品类别的链接
Match match = regex.Match(item.Attributes["href"].Value.ToLower());
if (match.Success)
{
//提取商品类别ID 商品类别链接 商品类别名称
listCategory.Add(new Category() { Id = iId++, JDId = match.Groups[1].Value, Name = item.InnerText, URL = item.Attributes["href"].Value.ToLower().StartsWith("http") ? item.Attributes["href"].Value : "https:" + item.Attributes["href"].Value });
}
}
}

抓取京东商品信息

流程

实现

获取数据

见类别获取部分实现。

解析数据

实体

1
2
3
4
5
6
7
8
9
public class Product
{
public int Id { get; set; }
public string JDId { get; set; }
public string Name { get; set; }
public string Price { get; set; }
public string ImageURL { get; set; }
public string URL { get; set; }
}

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
if (document != null)
{
//获取页码信息
string sCurrentPager = string.Empty;//当前页数
string sAllPager = string.Empty;//总页数
//List<Product> listProduct = new List<Product>();//商品
//获取页数信息
HtmlNodeCollection currentPagerNode = document.DocumentNode.SelectNodes("//*[@id=\"J_topPage\"]/span/b");
if (currentPagerNode.Count != 1)
{
throw new Exception("加载当前页码失败!");
}
else
{
sCurrentPager = currentPagerNode[0].InnerText;
}
HtmlNodeCollection allPagersNode = document.DocumentNode.SelectNodes("//*[@id=\"J_topPage\"]/span/i");
if (allPagersNode.Count != 1)
{
throw new Exception("加载总页数失败!");
}
else
{
sAllPager = allPagersNode[0].InnerText;
}
//获取商品信息
int iId = 1;//编号
HtmlNodeCollection productNode = document.DocumentNode.SelectNodes("//*[@id=\"plist\"]/ul/li");//符合条件的商品XPath语法
foreach (var item in productNode)
{
Product product = new Product();
HtmlAgilityPack.HtmlDocument tempDocument = new HtmlAgilityPack.HtmlDocument();
tempDocument.LoadHtml(item.OuterHtml);
//商品图片链接
HtmlNodeCollection imageNode = tempDocument.DocumentNode.SelectNodes("//*[@class=\"p-img\"]/a/img");
if (imageNode.Count > 0 && imageNode[0].Attributes["src"] != null && !string.IsNullOrEmpty(imageNode[0].Attributes ["src"].Value))// 正常加载图片链接
{
product.ImageURL = imageNode[0].Attributes["src"].Value.ToLower().StartsWith("http") ? imageNode[0].Attributes ["src"].Value : "https:" + imageNode[0].Attributes["src"].Value;
}
else if (imageNode.Count > 0 && imageNode[0].Attributes["data-lazy-img"] != null && !string.IsNullOrEmpty(imageNode [0].Attributes ["data-lazy-img"].Value))//懒加载图片链接
{
product.ImageURL = imageNode[0].Attributes["data-lazy-img"].Value.ToLower().StartsWith("http") ? imageNode[0].Attributes ["data- lazy-img"].Value : "https:" + imageNode[0].Attributes["data-lazy-img"].Value;
}
else
{
continue;
}
//商品ID
HtmlNodeCollection idNode = tempDocument.DocumentNode.SelectNodes("//li/div");
if (idNode.Count > 0 && idNode[0].Attributes["data-sku"] != null && !string.IsNullOrEmpty(idNode[0].Attributes["data- sku"].Value))
{
product.JDId = idNode[0].Attributes["data-sku"].Value;
}
else
{
continue;
}
//商品名称
HtmlNodeCollection nameNode = tempDocument.DocumentNode.SelectNodes("//*[@class=\"p-name\"]/a/em");
if (nameNode.Count > 0 && !string.IsNullOrEmpty(nameNode[0].InnerText.Trim()))
{
product.Name = nameNode[0].InnerText.Trim();
}
else
{
continue;
}
//商品链接
HtmlNodeCollection urlNode = tempDocument.DocumentNode.SelectNodes("//*[@class=\"p-name\"]/a");
if (urlNode.Count > 0 && urlNode[0].Attributes["href"] != null && !string.IsNullOrEmpty(urlNode[0].Attributes["href"].Value))
{
product.URL = urlNode[0].Attributes["href"].Value.ToLower().StartsWith("http") ? urlNode[0].Attributes["href"].Value : "https:" + urlNode[0].Attributes["href"].Value;
}
else
{
continue;
}
product.Id = iId++;
listProduct.Add(product);
}
}

注意事项

  • 图片懒加载,如果解析src属性可能解析不到,需要调试查看懒加载图片属性。
  • 价格信息抓取会发现为空,观察会发现其calss属性为“J_price”,怀疑为ajax请求获取,下一部分说明获取方法。

抓取京东商品价格信息

流程

  • 使用Chrome分析网络请求

    使用“F12 -> Network -> JS”分析页面刷新时的ajax请求,发现:https://p.3.cn/prices/mgets 的请求,怀疑为查询价格的请求信息

  • 分析请求链接

    过滤GET参数,发现传递参数skuIds即可获取信息,多个用“%2C”分隔即可,例如:https://p.3.cn/prices/mgets?skuIds=J_4609652%2CJ_5830869 ,获取的数据位JSON格式

  • 获取数据

    同抓取商品类别

  • 解析数据

    使用 Newtonsoft.Json 解析(Newtonsoft.Json 可以通过 nuget 添加引用)

实现

获取数据

见类别获取部分实现。

解析数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/// <summary>
/// 通过京东商品ID获取价格
/// </summary>
/// <param name="listJDId">商品ID</param>
/// <returns>商品ID对应价格的字典</returns>
private Dictionary<string, string> GetPrice(List<string> listJDId)
{
Dictionary<string, string> dicPrice = new Dictionary<string, string>();
if (listJDId.Count > 0)
{
//按照ajax请求连接格式拼接skuID
for (int i = 0; i < listJDId.Count; i++)
{
listJDId[i] = "J_" + listJDId[i];
}
//循环每10条查询一次 避免数量过多链接过长
for (int i = 0; i < (listJDId.Count + 9) / 10; i++)
{
//获取数据
HtmlAgilityPack.HtmlDocument document = new HtmlAgilityPack.HtmlDocument();
WebRequest request = WebRequest.Create("https://p.3.cn/prices/mgets?skuIds=" + string.Join("%2C", listJDId.Skip(i * 10).Take(10)));
using (Stream stream = request.GetResponse().GetResponseStream())
{
document.Load(stream, Encoding.UTF8);
}
//解析数据
if (!string.IsNullOrEmpty(document.Text))
{
object oPrice = Newtonsoft.Json.JsonConvert.DeserializeObject(document.Text);//转换成对象
if (oPrice is JArray)
{
foreach (var item in oPrice as JArray)
{
if (item is JObject && (item as JObject).ContainsKey("id") && (item as JObject).ContainsKey("p"))
{
//提取符合条件的数据
if ((item as JObject)["id"] is JValue jId && (item as JObject)["p"] is JValue jPrice && listJDId.Contains(jId.Value.ToString()) && !dicPrice.ContainsKey(jId.Value.ToString().TrimStart('J', '_')))
{
dicPrice.Add(jId.Value.ToString().TrimStart('J', '_'), jPrice.Value.ToString());
}
}
}
}
}
}
}
return dicPrice;
}

若想使用jQuery选择器可以结合Fizzlerex使用:https://archive.codeplex.com/?p=fizzlerex

入手Kindle

“好读书”

从小到大,自认为自己是一个不爱看书的人(网文除外),但是身为一个程序员,誓要向dalao看齐的人一直想培养一个良好的阅读习惯。

在工作的前期曾经购置了很多书,包括C#JavaScriptOracle的工具书还有一些名著、散文、豆瓣推荐等很多书,但是因为不方便携带,很多都在搬家的过程中遗失了,或者带回老家垫桌脚了。

从那以后,每次京东和当当有活动,都要纠结着要不要再剁手买几本,最后都忍了,因为这些书并不方便携带,而且选择困难症的我经常会纠结外出的时候带哪本书好。

“Kindle”软件

Kindle其实已经流行了好多年,但是身边认识的小伙伴几乎没有上手过的,所以也只是听说,知道有个“墨水屏”的技术。

真正用Kindle,是因为之前在Telegram发现了一个群,每天都会分享一些好书。开始用一个PC版基于Python开发的阅读软件,但是体验不好,使用没多久就放弃了。后来想起来Kindle好像可以在PC或移动设备上安装,于是便下载体验了一下,不得不说——真香,在手机、平板、PC上都安装了Kindle的软件。

而在体验了一段时间,很正常的操作是,买了Kindle Unlimited电子书包月服务,而且买了一年,哈哈。

当然买了以后,也并没有老老实实读书,啃了一本《白鹿原》后,渐渐又回到了上班摸鱼下班摸鱼的生活。

学习的“冲动”

一直想系统的学习一下平时接触的技术,并且了解一些一直感兴趣的领域。

比如系统的看一下JavaScriptC#MVC数据结构与算法23种设计模式等的工具书与教材。想要学习一下GitDockerPythonPowerShell等比较热门的技术。

这些书一般厚且贵,当然优选还是找电子书,先是在当当和京东选择了几本,但是后来想到了自己安装的Kindle,又放弃了下单。

纠结了很久,要不要买以及要不要考虑京东/iReader/当当家的电纸书阅读器,最终还在京东将Kindle加入了购物车。

购置

电子产品当然要在京东上购买才比较舒服,因为速度、售后、价格等服务上还是有一些优势的,而且……plus会员不能白买了。/(ㄒoㄒ)/~~

之前在知乎别人推荐,如果知识体验电纸书,推荐买入门版,但是为了“奖励”一下自己的上进心(钱都花了,不能白花一定要好好读书好好学习),果断——当然买不起3k+的版本,割肉买了32G的版本。

当然最终的价格是1288(买了个皮套)-50(plus会员满1200减50优惠券),入手价格是1238。(买之前忘记比价看一下这个点买有没有亏,买之后上去查了一下,感觉还好,价格一直很稳定。)

开箱

京东的服务就是快,上午十点下单,下午三点快递员就给我打了电话。当然比较不愉快的是之前都有送货上楼,这次居然让我去蜂巢自己取快递。😔

当然,Kindle的到来,开箱的喜悦,这些小插曲都是浮云哦。小心翼翼的开箱中……

kindle1

kindle2

啊?忘记了两个快递包裹,因为顺便补充了一下洗面奶的存货,看到大的盒子想当然的以为是Kindle,结果并不是,于是开另外一个包裹。

kindle3

包装很“节约”,盒子的大小感觉也就比Kindle本身大一点点。

kindle4

盒子上的图形刚开始还以为是水渍,后来才发现原来是平时Kindle上见到的封面图。

kindle5

一家人当然要整整齐齐的,6寸的大小和手掌的大小差不多,可以装进衣兜里。

kindle6

刚开始以为那些提示是贴膜,差点把屏幕掀起来……直接开机就好了。

kindle7

最终进系统就没什么好说的了,意料之中的延迟,可以接受。另外登录Amazon账号会有点问题,登录现有账号下面按钮显示的却是注册,而且回车以后一直提示失败,最终发现邮箱里躺着Amazon发送的“验证码”,用这个就可以登录。

阅读体验

刚到手剩余“50%”的电量,在将阅读灯开了20+的情况下,使用了大概八九个小时。感觉在续航上,不太满意,我理想的是充满电工作30h左右。

后面充了电,并将阅读灯关了再使用一下看看没有开阅读灯的续航怎么样,比较不理解的是为什么电纸书阅读器这个体积不提升一下电池,1500mAh还是有一些进步空间的。

因为本身有些近视,所以使用电子设备喜欢靠屏幕很近观看,长时间用Kindle阅读,明显没有用手机或者电脑那种眩目的感觉,体验非常不错。

不多说了,去看书了。😄

23种设计模式与常用设计模式

设计模式

设计模式

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。

目的是为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。

设计模式三大类型

设计模式分为三种类型,共23类。

  • 创建型模式:创建型模式用来处理对象的创建过程,单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
  • 结构型模式:结构型模式用来处理类或者对象的组合,适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
  • 行为型模式:行为型模式用来对类或对象怎样交互和怎样分配职责进行描述,模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。

单例模式

单例模式(Singleton Pattern)保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。

实现方式

饿汉式

第一时间创建实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/// <summary>
/// 静态构造函数
/// *该方法由CLR保证,程序第一次使用这个类型前被调用,且只调用一次
/// </summary>
public class Singleton2
{
static Singleton2()
{
Thread.Sleep(1000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")}: {nameof(Singleton2)}\t构造成功\t线程ID为{Thread.CurrentThread.ManagedThreadId}。");
}
//测试
public static void Show()
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")}: {nameof(Singleton2)}\t单例模式测试开始\t线程ID为{Thread.CurrentThread.ManagedThreadId}。");
Singleton2 singleton = null;
for (int i = 0; i < 10; i++)
{
Task.Factory.StartNew(() =>
{
singleton = new Singleton2();
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")}: {nameof(Singleton2)}\t获取单例\t线程ID为{Thread.CurrentThread.ManagedThreadId}。");
});
}
}
}
/// <summary>
/// 静态字段
/// *该方法由CLR保证,程序第一次使用这个类型前被初始化,且只初始化一次
/// </summary>
public class Singleton3
{
private Singleton3()
{
Thread.Sleep(1000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")}: {nameof(Singleton3)}\t构造成功\t线程ID为{Thread.CurrentThread.ManagedThreadId}。");
}
private static Singleton3 _singleton = new Singleton3();
public static Singleton3 CreateInstance()
{
return _singleton;
}
//测试
public static void Show()
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")}: {nameof(Singleton3)}\t单例模式测试开始\t线程ID为{Thread.CurrentThread.ManagedThreadId}。");
Singleton3 singleton = null;
for (int i = 0; i < 10; i++)
{
Task.Factory.StartNew(() =>
{
singleton = CreateInstance();
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")}: {nameof(Singleton3)}\t获取单例\t线程ID为{Thread.CurrentThread.ManagedThreadId}。");
});
}
}
}

懒汉式

需要才创建实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/// <summary>
/// 双if判断加锁
/// *内层if判断保证单例模式
/// *外层if判断避免加锁阻碍多线程运行
/// *加锁避免多线程对象被多次创建
/// </summary>
public class Singleton1
{
private Singleton1()
{
Thread.Sleep(1000);
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")}: {nameof(Singleton1)}\t构造成功\t线程ID为{Thread.CurrentThread.ManagedThreadId}。");
}
private static volatile Singleton1 _singleton = null;
private static object Singleton_Lock = new object();
public static Singleton1 CreateInstance()
{
if (_singleton == null)//不为空才去等待锁
{
lock (Singleton_Lock)
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")}: {nameof(Singleton1)}\t等待锁之后释放\t线程ID为{Thread.CurrentThread.ManagedThreadId}。");
Thread.Sleep(500);
if (_singleton == null)//保证不被重复创建
{
_singleton = new Singleton1();
}
}
}
return _singleton;
}

//测试
public static void Show()
{
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")}: {nameof(Singleton1)}\t单例模式测试开始\t线程ID为{Thread.CurrentThread.ManagedThreadId}。");
Singleton1 singleton = null;
List<Task> listTask = new List<Task>();
for (int i = 0; i < 10; i++)
{
listTask.Add(Task.Factory.StartNew(() =>
{
singleton = CreateInstance();
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")}: {nameof(Singleton1)}\t获取单例\t线程ID为{Thread.CurrentThread.ManagedThreadId}。");
}));
}
Task.WaitAll(listTask.ToArray());
for (int i = 0; i < 10; i++)
{
Task.Factory.StartNew(() =>
{
singleton = CreateInstance();
Console.WriteLine($"{DateTime.Now.ToString("HH:mm:ss")}: {nameof(Singleton1)}\t获取单例\t线程ID为{Thread.CurrentThread.ManagedThreadId}。");
});
}
}
}

三种工厂模式

简单工厂、工厂方法、抽象工厂都属于设计模式中的创建型模式。其主要功能都是帮助我们把对象的实例化部分抽象取了出来,优化了系统的架构,并且增强了系统的扩展性。

简单工厂

简单工厂(Simple Factory):简单工厂模式的工厂类一般是使用静态方法,通过接收的参数不同来返回不同的对象实例。不修改代码的话,是无法扩展的。

  • 优点:客户端可以免除直接创建产品对象的责任,而仅仅是“消费”产品。简单工厂模式通过这种做法实现了对责任的分割。
  • 缺点:由于工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中;它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了。

配置文件

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="BallType" value="Test.exe,Test.Factory.Basketball"/>
</appSettings>
</configuration>

实体与枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Sportsman
{
public Sportsman(string name, bool gender, int age, string country)
{
Name = name;
Gender = gender;
Age = age;
Country = country;
}
public string Name { get; set; }
public bool Gender { get; set; }
public int Age { get; set; }
public string Country { get; set; }
public void PlayGame(IBall ball)
{
Console.WriteLine($"You're watching {ball.PlayGame()} The name of the sportsman is {Name}. {(Gender ? "He" : "She")} is a {Age} years old {(Gender ? "boy" : "girl")} from {Country}.");
}
}
public interface IBall
{
string PlayGame();
}
public enum BallType
{
Basketball,
Football,
Baseball,
Volleyball
}

简单工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
public class SimpleFactory
{
public static void Show()
{
Sportsman sportsman = new Sportsman("Kangkang", true, 17, "China");
{
Basketball basketball = new Basketball();//1.左右都是细节
sportsman.PlayGame(basketball);
}
{
IBall ball = new Football();//2.左边是抽象 右边是细节
sportsman.PlayGame(ball);
}
{
//遵循依赖倒置 但是违背了单一职责
Console.WriteLine("********简单工厂-转移细节********");
IBall ball = CreateBallGame(BallType.Volleyball);//3.没有细节 细节被转移
sportsman.PlayGame(ball);
}
{
//结合配置项+反射 IOC雏形
Console.WriteLine("********简单工厂-配置项********");
IBall ball = CreateBallFromConfiguration();
sportsman.PlayGame(ball);
}
}

/// <summary>
/// **集中了矛盾**
/// 细节没有消失只是转移
/// 转移了矛盾并没有消除矛盾
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static IBall CreateBallGame(BallType type)
{
IBall ball = null;
switch (type)
{
case BallType.Basketball:
ball = new Basketball();
break;
case BallType.Football:
ball = new Football();
break;
case BallType.Baseball:
ball = new Baseball();
break;
case BallType.Volleyball:
ball = new Volleyball();
break;
default:
throw new Exception("unknown ball type.");
}
return ball;
}

/// <summary>
/// IOC的雏形
/// </summary>
/// <returns></returns>
public static IBall CreateBallFromConfiguration()
{
string[] aTemp = ConfigurationManager.AppSettings.Get("BallType").Split(',');
Assembly assembly = Assembly.LoadFrom(aTemp[0]);
Type type = assembly.GetType(aTemp[1]);
IBall ball = Activator.CreateInstance(type) as IBall;
return ball;
}
}

public class Baseball : IBall
{
public string PlayGame()
{
return "a baseball tv show.";
}
}
public class Football : IBall
{
public string PlayGame()
{
return "a football tv show.";
}
}
public class Baseball : IBall
{
public string PlayGame()
{
return "a baseball tv show.";
}
}
public class Volleyball : IBall
{
public string PlayGame()
{
return "a volleyball tv show.";
}
}

简单工厂实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class SimpleFactory
{
public static void Show()
{
Sportsman sportsman = new Sportsman("Kangkang", true, 17, "China");
{
Basketball basketball = new Basketball();//1.左右都是细节
sportsman.PlayGame(basketball);
}
{
IBall ball = new Football();//2.左边是抽象 右边是细节
sportsman.PlayGame(ball);
}
{
//遵循依赖倒置 但是违背了单一职责
Console.WriteLine("********简单工厂-转移细节********");
IBall ball = CreateBallGame(BallType.Volleyball);//3.没有细节 细节被转移
sportsman.PlayGame(ball);
}
{
//结合配置项+反射 IOC雏形
Console.WriteLine("********简单工厂-配置项********");
IBall ball = CreateBallFromConfiguration();
sportsman.PlayGame(ball);
}
}

/// <summary>
/// **集中了矛盾**
/// 细节没有消失只是转移
/// 转移了矛盾并没有消除矛盾
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static IBall CreateBallGame(BallType type)
{
IBall ball = null;
switch (type)
{
case BallType.Basketball:
ball = new Basketball();
break;
case BallType.Football:
ball = new Football();
break;
case BallType.Baseball:
ball = new Baseball();
break;
case BallType.Volleyball:
ball = new Volleyball();
break;
default:
throw new Exception("unknown ball type.");
}
return ball;
}

/// <summary>
/// IOC的雏形
/// </summary>
/// <returns></returns>
public static IBall CreateBallFromConfiguration()
{
string[] aTemp = ConfigurationManager.AppSettings.Get("BallType").Split(',');
Assembly assembly = Assembly.LoadFrom(aTemp[0]);
Type type = assembly.GetType(aTemp[1]);
IBall ball = Activator.CreateInstance(type) as IBall;
return ball;
}
}

工厂方法

工厂方法(Factory Method):工厂方法是针对每一种产品提供一个工厂类。通过不同的工厂实例来创建不同的产品实例。在同一等级结构中,支持增加任意产品。

  • 优点:允许系统在不修改具体工厂角色的情况下引进新产品。
  • 缺点:由于每加一个产品,就需要加一个产品工厂的类,增加了额外的开发量。

提供工厂类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public interface IFactory
{
IBall CreateBall();
}
public class BaseballFactory : IFactory
{
public IBall CreateBall()
{
return new Baseball();
}
}
public class FootballFactory : IFactory
{
public IBall CreateBall()
{
return new Football();
}
}
public class BaseballFactory : IFactory
{
public IBall CreateBall()
{
return new Baseball();
}
}
public class VolleyballFactory : IFactory
{
public IBall CreateBall()
{
return new Volleyball();
}
}

工厂方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FactoryMethod
{
public static void Show()
{
Sportsman sportsman = new Sportsman("Kangkang", true, 17, "China");
{
//每个业务对应一个工厂 将业务细节的依赖转移到中间层(工厂)
//优点:允许系统在不修改具体工厂角色的情况下引进新产品
//缺点:由于每加一个产品,就需要加一个产品工厂的类,增加了额外的开发量
Console.WriteLine("********工厂方法********");
IFactory factory = new BasketballFactory();
IBall ball = factory.CreateBall();
sportsman.PlayGame(ball);
}
}
}

抽象工厂

抽象工厂(Abstract Factory):抽象工厂是应对产品族概念的。应对产品族概念而生,增加新的产品线很容易,但是无法增加新的产品。比如,每个汽车公司可能要同时生产轿车、货车、客车,那么每一个工厂都要有创建轿车、货车和客车的方法。

  • 优点:向客户端提供一个接口,使得客户端在不必指定产品具体类型的情况下,创建多个产品族中的产品对象。
  • 缺点:增加新的产品等级结构很复杂,需要修改抽象工厂和所有的具体工厂类,对“开闭原则”的支持呈现倾斜性。

例如游戏加载需要英雄、资源、技能等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public abstract class AbstractFactory
{
public abstract IHero CreateHero();
public abstract ISkill CreateSkill();
public abstract IResource CreateResource();
public abstract IWeapon CreateWeapon();
}

public interface IHero
{
string ShowHero();
}

public interface ISkill
{
string ShowSkill();
}

public interface IResource
{
string ShowResource();
}

public interface IWeapon
{
string ShowWeapon();
}

装饰器模式

装饰器模式(Decorator Pattern):允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。动态地给一个对象添加一些额外的职责。

就增加功能来说,装饰器模式相比生成子类更为灵活。一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。

类与类之间的关系: 纵向有继承和实现,横向有依赖、关联、组合、聚合。结构型设计模式:组合优于继承

  • 优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
  • 缺点:多层装饰比较复杂。

学生学习免费或付费课程享有不同的权利,除享受免费课程外,付费学生还有课前资料预习、课后作业指导、资料复习。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public abstract class AbstractStudent
{
public int Id { get; set; }
public string Name { get; set; }
public abstract void Study();

public static void Show()
{
AbstractStudent student = new VIPStudent() { Id = 999, Name = "Kangkang" };
student.Study();
Console.WriteLine("************************");
StudentPreviewDecorator previewStudent = new StudentPreviewDecorator(student);
previewStudent.Study();
Console.WriteLine("************************");
AbstractStudent abstractStudent = new StudentPreviewDecorator(student);
abstractStudent.Study();
Console.WriteLine("************************");
student = new StudentPayDecorator(student);
student = new StudentPreviewDecorator(student);
student = new StudentHomeworkDecorator(student);
student = new StudentReviewDecorator(student);
student.Study();
}
}

public class PoolStudent : AbstractStudent
{
public override void Study()
{
Console.WriteLine("Take free courses.");
}
}

public class VIPStudent : AbstractStudent
{
public override void Study()
{
Console.WriteLine("Take free and paid courses.");
}
}

继承也可以实现,但是不够灵活

1
2
3
4
5
6
7
8
9
public class BaseStudentInherit : VIPStudent
{
public override void Study()
{
Console.WriteLine("Pay.");
Console.WriteLine("Preview.");
base.Study();
}
}

付费、预习、作业、复习等装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class StudentPayDecorator : BaseStudentDecorator
{
public StudentPayDecorator(AbstractStudent student) : base(student)
{ }
public override void Study()
{
Console.WriteLine("Pay.");
base.Study();
}
}
public class StudentPreviewDecorator : BaseStudentDecorator
{
public StudentPreviewDecorator(AbstractStudent student) : base(student)
{
}
public override void Study()
{
Console.WriteLine("Preview.");
base.Study();
}
}
public class StudentHomeworkDecorator : BaseStudentDecorator
{
public StudentHomeworkDecorator(AbstractStudent student) : base(student)
{ }

public override void Study()
{
base.Study();
Console.WriteLine("Do homework.");
}
}
public class StudentReviewDecorator:BaseStudentDecorator
{
public StudentReviewDecorator(AbstractStudent student) : base(student)
{ }

public override void Study()
{
base.Study();
Console.WriteLine("Review.");
}
}

代理模式

代理模式(Proxy Pattern):一个类代表另一个类的功能,为其他对象提供一种代理以控制对这个对象的访问。主要解决在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。

在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。

优点:

  • 职责清晰。
  • 高扩展性。
  • 智能化。

缺点:

  • 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
  • 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

代理模式:只能传达原有逻辑,不能新增业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public interface ISubject
{
bool GetSomething();
bool DoSomething();
}
public class RealSubject : ISubject
{
public bool DoSomething()
{
Console.WriteLine("Passenger by train.");
return true;
}

public bool GetSomething()
{
Console.WriteLine("Passengers buy tickets.");
return true;
}
}
public class ProxySubject : ISubject
{
private ISubject _subject = new RealSubject();
public bool DoSomething()
{
Console.WriteLine("Before do something.");
bool result = _subject.DoSomething();
Console.WriteLine("After do something.");
return result;
}

public bool GetSomething()
{
Console.WriteLine("Before get something.");
bool result = _subject.GetSomething();
Console.WriteLine("After get something.");
return result;
}

public static void Show()
{
ISubject realSubject = new RealSubject();
realSubject.GetSomething();
realSubject.DoSomething();
Console.WriteLine("************************");
ISubject proxySubject = new ProxySubject();
proxySubject.GetSomething();
proxySubject.DoSomething();
}
}

观察者模式

观察者模式(Observer Pattern):当对象间存在一对多关系时,则使用观察者模式。比如,当一个对象被修改时,则会自动通知它的依赖对象。

观察者模式属于行为型模式。定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

主要解决一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

优点:

  • 观察者和被观察者是抽象耦合的。
  • 建立一套触发机制。

缺点:

  • 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
  • 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

一只神奇的猫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/// <summary>
/// 一直神奇的猫,猫叫之后触发:
/// Baby cry
/// Brother turn
/// Dog woof
/// Father roar
/// Mother whisper
/// Mouse run
/// Neighbor awake
/// Stealer hide
/// </summary>
public class Cat
{
public void Meow()
{
Console.WriteLine($"{GetType().Name} meow...");
new Brother().Turn();
new Dog().Woof();
new Father().Roar();
new Mother().Whisper();
new Mouse().Run();
new Neighbor().Awake();
new Stealer().Hide();
}

public event Action MeowHandler;
public void MeowEvent()
{
Console.WriteLine($"{GetType().Name} meow...");
MeowHandler?.Invoke();
}

private List<IObserver> _listObserver = new List<IObserver>();
public void Add(IObserver observer)
{
_listObserver.Add(observer);
}
public void Remove(IObserver observer)
{
_listObserver.Remove(observer);
}
public void MeowObserver()
{
Console.WriteLine($"{GetType().Name} meow...");
foreach (var item in _listObserver)
{
item.Action();
}
}

public static void Show()
{
Console.WriteLine("************普通方法************");
Cat cat = new Cat();
cat.Meow();
Console.WriteLine("************ 事 件 ************");
cat.MeowHandler += () => new Brother().Turn();
cat.MeowHandler += () => new Dog().Woof();
cat.MeowHandler += () => new Father().Roar();
cat.MeowHandler += () => new Mother().Whisper();
cat.MeowHandler += () => new Mouse().Run();
cat.MeowHandler += () => new Neighbor().Awake();
cat.MeowHandler += () => new Stealer().Hide();
cat.MeowEvent();
Console.WriteLine("************观 察 者************");
cat.Add(new Brother());
cat.Add(new Dog());
cat.Add(new Father());
cat.Add(new Mother());
cat.Add(new Mouse());
cat.Add(new Neighbor());
cat.Add(new Stealer());
cat.MeowObserver();
}
}
public interface IObserver
{
void Action();
}
public class Baby : IObserver
{
public void Action()
{
Cry();
}

public void Cry()
{
Console.WriteLine($"{GetType().Name} cry...");
}
}
public class Brother : IObserver
{
public void Action()
{
Turn();
}

public void Turn()
{
Console.WriteLine($"{GetType().Name} turn...");
}
}
public class Dog : IObserver
{
public void Action()
{
Woof();
}

public void Woof()
{
Console.WriteLine($"{GetType().Name} Woof...");
}
}
public class Father : IObserver
{
public void Action()
{
Roar();
}

public void Roar()
{
Console.WriteLine($"{GetType().Name} roar...");
}
}
public class Mother : IObserver
{
public void Action()
{
Whisper();
}

public void Whisper()
{
Console.WriteLine($"{GetType().Name} whisper...");
}
}
public class Mouse : IObserver
{
public void Action()
{
Run();
}

public void Run()
{
Console.WriteLine($"{GetType().Name} Run...");
}
}
public class Neighbor : IObserver
{
public void Action()
{
Awake();
}

public void Awake()
{
Console.WriteLine($"{GetType().Name} awake...");
}
}
public class Stealer : IObserver
{
public void Action()
{
Hide();
}

public void Hide()
{
Console.WriteLine($"{GetType().Name} hide...");
}
}

设计模式六大原则

单一职责原则

Single Responsibility Principle 简称 :SRP

定义:应该有且仅有一个原因引起类的变更。

接口的职责在设计时应该做到单一,降低类的复杂性,实现的职责都有明确的定义,提高了可读性、可维护性、可扩展性;

变更引起的风险降低,如果接口隔离性做的好,一个接口的修改只对相应实现类有影响,对其它接口没有影响,降低了耦合性。

单一职责提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计的是否优良,但是“职责”或“变化原因”都是不可度量的,因项目而异,因环境而异。

里氏替换原则

Liskov Substitution Principle 简称:LSP

定义:是面向对象设计的基本原则之一,只要父类能出现的地方子类就能出现,而且替换为子类也不会产生任何错误或异常。

  • 子类必须完全实现父类的方法
  • 子类可以有自己的个性
  • 覆盖或实现父类的方法时输入参数可以被放大
  • 覆盖或实现父类的方法时输出结果可以被缩小

优点:

  • 代码共享,减少创建类的工作量
  • 提高代码的重用性
  • 子类可以形似父类,但又异于父类
  • 提高代码的可扩展性
  • 提高产品或项目的开放性

缺点:

  • 继承是侵入性的,只要继承就必须拥有父类的所有属性和方法
  • 降低代码的灵活性
  • 增强了耦合性,父类的常量、变量、方法被修改时,要考虑到子类的修改,在缺乏规范的环境下,可能有许多代码需要重构。

LSP 原则是继承复用的基石,只有当子类能够替换掉父类,软件单位功能不受影响时,父类才能被真正复用。LSP 原则是对开闭原则的补充,实现开闭原则的关键步骤就是抽象化,而父类与子类的继承关 系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

依赖倒置原则

Dependence Inversion Principle 简称:DIP

定义:是“面向接口编程” –面向对象设计的精髓之一,是六个原则中最难实现的一个原则,是实现开闭原则的重要途径。

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖是通过接口和抽象类发生的
  • 接口或抽象类不依赖于实现类
  • 实现类依赖于接口或抽象类

优点:

  • 可以减少类之间的耦合性,提高系统的稳定性
  • 降低并行开发引起的风险
  • 提高代码的可读性和可维护性

依赖的三种写法:

  • 构造函数传递依赖对象
  • setter方法传递依赖对象
  • 接口声明依赖对象

本质是通过抽象使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合,应遵循的原则:

  • 每个类尽量都有接口或抽象类
  • 变量的表面类型尽量是接口或者抽象类
  • 任何类都不应从具体类中派生
  • 尽量不要复写基类的方法
  • 结合里氏替换原则使用

依赖倒置原则是设计模式;
控制反转(IoC:将组件间的依赖关系从程序内部提到外部来管理)是目的;
依赖注入(DI:将组件的依赖通过外部以参数或其他形式注入)是实现控制反转的方式。

接口隔离原则

Interface Segregation Principle 简称:ISP

定义:客户端不应依赖它不需要的接口,类间的依赖关系应建立在最小的接口上(降低耦合度)

  • 接口尽量小(拆分接口时,首先必须满足单一职责原则)
  • 接口要高内聚(尽量少公布 public 方法)
  • 定制服务(模块间必然会有耦合,有耦合就要有相互访问的接口,设计时应为访问者提供定制服务)
  • 接口的设计是有限度的(接口的设计粒度越小,接口越灵活,要把握一个度)

实践中可根据以下几个规则衡量:

  • 一个接口只服务于一个子模块或业务逻辑
  • 通过业务逻辑压缩接口中的public方法
  • 已经被污染的接口,尽量去修改,若变更风险较大,则采用适配器模式进行转化处理
  • 了解环境,拒绝盲从,环境不同,接口拆分的标准就不同,应深入了解业务逻辑,设计出灵活的接口

迪米特法则

Law of Demeter 简称:LoD)又称最少知识原则

定义:一个类应该对自己需要耦合或调用的类知道的最少

  • 只和直接的朋友交流
  • 朋友之间也是有距离的
  • 是自己的就是自己的(如果一个方法放在本类中,既不增加类间的关系,也对本类不产生负面影响,那就放在本类中)
  • 谨慎使用 Serializable (远程调用传个vo,必须实现 Serializable 接口,如果有一天修改了访问权限,权限扩大了,但是服务器上没有做相应的变更,就会报序列化失败)

开闭原则

(Open-Closed Principle 简称:OCP)

定义:对扩展开放,对修改关闭,在程序需要扩展的时候,不能修改原有的代码,实现热插拔的效果。为了使程序的扩展性好,易于维护和升级,需要使用接口和抽象类。

注意事项:

  • 通过对接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法
  • 参数类型、引用对象尽量使用接口或抽象类,而不是实现类
  • 抽象层尽量保持稳定,一旦确定不允许修改

如何使用:

  • 抽象约束(实现对扩展开放的首要前提,对扩展进行边界限定)
  • 元数据控制模块行为
  • 制定项目章程(对项目来说,规定优于配置)
  • 封装变化
    • 将相同的变化封装到一个接口或抽象类中;
    • 将不同的变化封装到不同的接口或抽象类中;

AOP 面向切面编程

AOP 面向切面编程

OOP 面向对象编程的关系

AOPAspect-Oriented Programming,面向方面编程),可以说是 OOP(Object-Oriented Programing ,面向对象编程)的补充和完善。

OOP 引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP 则显得无能为力。也就是说, OOP 允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在 OOP 设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP 技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。

所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP 代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。

使用“横切”技术,AOP 把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。Aop 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。正如 Avanade 公司的高级方案构架师 Adam Magee 所说,AOP 的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。”

AOP 技术的实现

主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。然而殊途同归,实现 AOP 的技术特性却是相同的。

装饰器模式实现静态代理

AOP 在方法的前后增加自定义的方法。详见:Decorator.Show() 方法,静态装饰器实现,并不推荐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public static class Decorator
{
public static void Show()
{
User user = new User() { Name = "John", Password = "12345678" };
IUserProcessor processor = new UserProcessor();
Console.WriteLine("******使用普通方法完成注册******");
processor.RegistUser(user);
Console.WriteLine("******使用装饰器模式完成注册******");
processor = new UserProcessorDecorator(processor);
processor.RegistUser(user);
}

public class User
{
public string Name { get; set; }
public string Password { get; set; }
}
public interface IUserProcessor
{
void RegistUser(User user);
}
public class UserProcessor : IUserProcessor
{
public void RegistUser(User user)
{
Console.WriteLine($"成功注册用户:{user.Name} 密码:{user.Password}");
}
}
/// <summary>
/// 装饰器模式去提供一个AOP功能
/// </summary>
public class UserProcessorDecorator : IUserProcessor
{
private IUserProcessor UserProcessor { get; set; }
public UserProcessorDecorator(IUserProcessor processor)
{
UserProcessor = processor;
}
public void RegistUser(User user)
{
PreProceed(user);
try
{
this.UserProcessor.RegistUser(user);
}
catch (Exception)
{
throw;
}
PostProced(user);
}

private void PreProceed(User user)
{
Console.WriteLine($"注册用户:{user.Name} 密码:{user.Password} 之前");
}

private void PostProced(User user)
{
Console.WriteLine($"注册用户:{user.Name} 密码:{user.Password} 之后");
}
}
}

使用 .Net Remoting/RealProxy 实现动态代理

详见 Proxy.Show() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public static class Proxy
{
public static void Show()
{
User user = new User() { Name = "John", Password = "12345678" };
IUserProcessor processor = new UserProcessor();
Console.WriteLine("******使用普通方法完成注册******");
processor.RegistUser(user);
Console.WriteLine("******使用动态代理完成注册******");
UserProcessor userProcessor = TransparentProxy.Create<UserProcessor>();
userProcessor.RegistUser(user);
}

public class User
{
public string Name { get; set; }
public string Password { get; set; }
}
public interface IUserProcessor
{
void RegistUser(User user);
}
public class UserProcessor : MarshalByRefObject, IUserProcessor
{
public void RegistUser(User user)
{
Console.WriteLine($"成功注册用户:{user.Name} 密码:{user.Password}");
}
}

public class MyRealProxy<T> : RealProxy
{
private T tTarget;
public MyRealProxy(T target) : base(typeof(T))
{
this.tTarget = target;
}
public override IMessage Invoke(IMessage msg)
{
PreProceed(msg);
IMethodCallMessage callMessage = (IMethodCallMessage)msg;
object returnValue = callMessage.MethodBase.Invoke(this.tTarget, callMessage.Args);
PostProced(msg);
return new ReturnMessage(returnValue, new object[0], 0, null, callMessage);
}

private void PreProceed(IMessage msg)
{
IMethodCallMessage callMessage = (IMethodCallMessage)msg;
StringBuilder sbInfo = new StringBuilder();
if (callMessage.Args.Length > 0)
{
PropertyInfo[] props = callMessage.Args[0].GetType().GetProperties();
for (int i = 0; i < props.Length; i++)
{
sbInfo.Append($"{props[i].Name}:{props[i].GetValue(callMessage.Args[0], null)} ");
}
}
Console.WriteLine($"方法执行前:{sbInfo.ToString().TrimEnd()}");
}

private void PostProced(IMessage msg)
{
IMethodCallMessage callMessage = (IMethodCallMessage)msg;
StringBuilder sbInfo = new StringBuilder();
if (callMessage.Args.Length > 0)
{
PropertyInfo[] props = callMessage.Args[0].GetType().GetProperties();
for (int i = 0; i < props.Length; i++)
{
sbInfo.Append($"{props[i].Name}:{props[i].GetValue(callMessage.Args[0], null)} ");
}
}
Console.WriteLine($"方法执行后:{sbInfo.ToString().TrimEnd()}");
}
}

/// <summary>
/// TransparentProxy
/// </summary>
public static class TransparentProxy
{
public static T Create<T>()
{
T instance = Activator.CreateInstance<T>();
MyRealProxy<T> proxy = new MyRealProxy<T>(instance);
T transparentProxy = (T)proxy.GetTransparentProxy();
return transparentProxy;
}
}
}

使用 Castle/DynamicProxy 实现动态代理

详见 CastleProxy.Show() 方法。(nuget 安装 Castle.DynamicProxyCastle.Core

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class CastleProxy
{
public static void Show()
{
User user = new User() { Name = "John", Password = "12345678" };
IUserProcessor processor = new UserProcessor();
Console.WriteLine("******使用普通方法完成注册******");
processor.RegistUser(user);
Console.WriteLine("******使用动态代理完成注册******");
ProxyGenerator generator = new ProxyGenerator();
MyInterceptor interceptor = new MyInterceptor();
processor = generator.CreateClassProxy<UserProcessor>(interceptor);
processor.RegistUser(user);
}

public class User
{
public string Name { get; set; }
public string Password { get; set; }
}
public interface IUserProcessor
{
void RegistUser(User user);
}
public class UserProcessor : MarshalByRefObject, IUserProcessor
{
/// <summary>
/// 必须是虚方法
/// </summary>
/// <param name="user"></param>
public virtual void RegistUser(User user)
{
Console.WriteLine($"成功注册用户:{user.Name} 密码:{user.Password}");
}
}

public class MyInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
PreProceed(invocation);
invocation.Proceed();
PostProced(invocation);
}

private void PreProceed(IInvocation invocation)
{
StringBuilder sbInfo = new StringBuilder();
if (invocation.Arguments.Length > 0)
{
PropertyInfo[] props = invocation.Arguments[0].GetType().GetProperties();
for (int i = 0; i < props.Length; i++)
{
sbInfo.Append($"{props[i].Name}:{props[i].GetValue(invocation.Arguments[0], null)} ");
}
}
Console.WriteLine($"方法执行前:{sbInfo.ToString().TrimEnd()}");
}

private void PostProced(IInvocation invocation)
{
StringBuilder sbInfo = new StringBuilder();
if (invocation.Arguments.Length > 0)
{
PropertyInfo[] props = invocation.Arguments[0].GetType().GetProperties();
for (int i = 0; i < props.Length; i++)
{
sbInfo.Append($"{props[i].Name}:{props[i].GetValue(invocation.Arguments[0], null)} ");
}
}
Console.WriteLine($"方法执行后:{sbInfo.ToString().TrimEnd()}");
}
}
}

使用 EntLib/PIBA Unity 实现动态代理

详见 UnityAOP.Show() 方法。(nuget 安装Unity 4.0.1Unity.Interception

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
public class UnityAOP
{
public static void Show()
{
User user = new User() { Name = "John", Password = "12345678" };
IUserProcessor processor = new UserProcessor();
Console.WriteLine("******使用普通方法完成注册******");
processor.RegistUser(user);

Console.WriteLine("******使用动态代理完成注册******");
IUnityContainer container = new UnityContainer();//声明一个容器
container.RegisterType<IUserProcessor, UserProcessor>();//声明UnityContainer
processor = container.Resolve<IUserProcessor>();
processor.RegistUser(user);//调用

//AOP
container.AddNewExtension<Interception>().Configure<Interception>().SetInterceptorFor<IUserProcessor>(new InterfaceInterceptor());
processor = container.Resolve<IUserProcessor>();
processor.RegistUser(user);
}

#region 特性
public class UserHanlerAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
ICallHandler handler = new UserHandler() { Order = this.Order };
return handler;
}
}

public class LogHandlerAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
ICallHandler handler = new LogHandler() { Order = this.Order };
return handler;
}
}

public class ExceptionHandlerAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
ICallHandler handler = new ExceptionHandler() { Order = this.Order };
return handler;
}
}

public class AfterLogHandlerAttributeAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
ICallHandler handler = new AfterLogHandler() { Order = this.Order };
return handler;
}
}
#endregion

#region 特性对应的行为
public class UserHandler : ICallHandler
{
public int Order { get; set; }

public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
Console.WriteLine("this is the user handler.");
User user = input.Arguments[0] as User;
Console.WriteLine($"regist user Name: {user.Name} Password: {user.Password}");
if (string.IsNullOrWhiteSpace(user.Name))
{
throw new Exception("user name can't be null, empty or white space.");
}
if (string.IsNullOrWhiteSpace(user.Password) || user.Password.Length < 8 || user.Password.Length > 32)
{
throw new Exception("user password can't be null, empty or white space, and the password's length must between 8-32.");
}
//return getNext.Invoke().Invoke(input, getNext);
return getNext()(input, getNext);
}
}

public class LogHandler : ICallHandler
{
public int Order { get; set; }

public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
Console.WriteLine("this is the log handler.");
User user = input.Arguments[0] as User;
Console.WriteLine($"Name: {user.Name} Password: {user.Password}");
return getNext()(input, getNext);
}
}

public class ExceptionHandler : ICallHandler
{
public int Order { get; set; }

public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
IMethodReturn methodReturn = getNext()(input, getNext);
Console.WriteLine("this is the exception handler.");
User user = input.Arguments[0] as User;
if (methodReturn.Exception == null)
{
Console.WriteLine($"end regist user. Name: {user.Name} Password: {user.Password} don't have exception.");
}
else
{
Console.WriteLine($"end regist user. Name: {user.Name} Password: {user.Password} have a exception: {methodReturn.Exception.Message}");
}
return methodReturn;
}
}

public class AfterLogHandler : ICallHandler
{
public int Order { get; set; }

public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
Console.WriteLine("this is the end log handler.");
User user = input.Arguments[0] as User;
Console.WriteLine($"end regist user. Name: {user.Name} Password: {user.Password}");
return getNext()(input, getNext);
}
}
#endregion

public class User
{
public string Name { get; set; }
public string Password { get; set; }
}

[UserHanler(Order = 1), ExceptionHandler(Order = 3), LogHandler(Order = 2), AfterLogHandlerAttribute(Order = 5)]
public interface IUserProcessor
{
void RegistUser(User user);
void GetUser(User user);
}
public class UserProcessor : IUserProcessor
{
public void GetUser(User user)
{
throw new NotImplementedException();
}

public void RegistUser(User user)
{
Console.WriteLine($"成功注册用户:{user.Name} 密码:{user.Password}");
}
}

/*
* TransparentProxyInterceptor:直接在类的方法上进行标记,但是这个类必须继承MarshalByRefObject,不建议使用。
* VirtualMethod:直接在类的方法上进行标记,但是这个方法必须是虚方法。
* InterfaceInterceptor:在接口的方法上进行标记,这样继承这个接口的类里实现这个接口方法的方法就能被拦截。
*/
}

以上四种方式,前三种理解,第四种需要掌握。